/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 at recompile
  • Date: 2020-02-07 20:53:34 UTC
  • Revision ID: teddy@recompile.se-20200207205334-dp41p8c8vw0ytik5
Allow users to more easily alter mandos.service

The sysvinit script uses /etc/default/mandos as an environment file,
and supports adding additional server options to a DAEMON_ARGS
environment variable.  This should be supported by the systemd
service, too.

* mandos.service ([Service]/EnvironmentFile): New; set to
  "/etc/default/mandos ".
  ([Service]/ExecStart): Append "$DAEMON_ARGS".

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
# -*- mode: python; after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*-
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-2017 Teddy Hogeborn
15
 
# Copyright © 2008-2017 Björn Påhlsson
 
14
# Copyright © 2008-2019 Teddy Hogeborn
 
15
# Copyright © 2008-2019 Björn Påhlsson
16
16
#
17
17
# This file is part of Mandos.
18
18
#
77
77
import itertools
78
78
import collections
79
79
import codecs
 
80
import unittest
 
81
import random
80
82
 
81
83
import dbus
82
84
import dbus.service
 
85
import gi
83
86
from gi.repository import GLib
84
87
from dbus.mainloop.glib import DBusGMainLoop
85
88
import ctypes
87
90
import xml.dom.minidom
88
91
import inspect
89
92
 
 
93
if sys.version_info.major == 2:
 
94
    __metaclass__ = type
 
95
    str = unicode
 
96
 
 
97
# Add collections.abc.Callable if it does not exist
 
98
try:
 
99
    collections.abc.Callable
 
100
except AttributeError:
 
101
    class abc:
 
102
        Callable = collections.Callable
 
103
    collections.abc = abc
 
104
    del abc
 
105
 
 
106
# Show warnings by default
 
107
if not sys.warnoptions:
 
108
    import warnings
 
109
    warnings.simplefilter("default")
 
110
 
90
111
# Try to find the value of SO_BINDTODEVICE:
91
112
try:
92
113
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
112
133
            # No value found
113
134
            SO_BINDTODEVICE = None
114
135
 
115
 
if sys.version_info.major == 2:
116
 
    str = unicode
 
136
if sys.version_info < (3, 2):
 
137
    configparser.Configparser = configparser.SafeConfigParser
117
138
 
118
 
version = "1.7.15"
 
139
version = "1.8.9"
119
140
stored_state_file = "clients.pickle"
120
141
 
121
142
logger = logging.getLogger()
 
143
logging.captureWarnings(True)   # Show warnings via the logging system
122
144
syslogger = None
123
145
 
124
146
try:
179
201
    pass
180
202
 
181
203
 
182
 
class PGPEngine(object):
 
204
class PGPEngine:
183
205
    """A simple class for OpenPGP symmetric encryption & decryption"""
184
206
 
185
207
    def __init__(self):
189
211
            output = subprocess.check_output(["gpgconf"])
190
212
            for line in output.splitlines():
191
213
                name, text, path = line.split(b":")
192
 
                if name == "gpg":
 
214
                if name == b"gpg":
193
215
                    self.gpg = path
194
216
                    break
195
217
        except OSError as e:
200
222
                          '--force-mdc',
201
223
                          '--quiet']
202
224
        # Only GPG version 1 has the --no-use-agent option.
203
 
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
 
225
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
204
226
            self.gnupgargs.append("--no-use-agent")
205
227
 
206
228
    def __enter__(self):
275
297
 
276
298
 
277
299
# Pretend that we have an Avahi module
278
 
class Avahi(object):
279
 
    """This isn't so much a class as it is a module-like namespace.
280
 
    It is instantiated once, and simulates having an Avahi module."""
 
300
class avahi:
 
301
    """This isn't so much a class as it is a module-like namespace."""
281
302
    IF_UNSPEC = -1               # avahi-common/address.h
282
303
    PROTO_UNSPEC = -1            # avahi-common/address.h
283
304
    PROTO_INET = 0               # avahi-common/address.h
287
308
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
288
309
    DBUS_PATH_SERVER = "/"
289
310
 
290
 
    def string_array_to_txt_array(self, t):
 
311
    @staticmethod
 
312
    def string_array_to_txt_array(t):
291
313
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
292
314
                           for s in t), signature="ay")
293
315
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
298
320
    SERVER_RUNNING = 2           # avahi-common/defs.h
299
321
    SERVER_COLLISION = 3         # avahi-common/defs.h
300
322
    SERVER_FAILURE = 4           # avahi-common/defs.h
301
 
avahi = Avahi()
302
323
 
303
324
 
304
325
class AvahiError(Exception):
316
337
    pass
317
338
 
318
339
 
319
 
class AvahiService(object):
 
340
class AvahiService:
320
341
    """An Avahi (Zeroconf) service.
321
342
 
322
343
    Attributes:
496
517
class AvahiServiceToSyslog(AvahiService):
497
518
    def rename(self, *args, **kwargs):
498
519
        """Add the new name to the syslog messages"""
499
 
        ret = AvahiService.rename(self, *args, **kwargs)
 
520
        ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
500
521
        syslogger.setFormatter(logging.Formatter(
501
522
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
502
523
            .format(self.name)))
504
525
 
505
526
 
506
527
# Pretend that we have a GnuTLS module
507
 
class GnuTLS(object):
508
 
    """This isn't so much a class as it is a module-like namespace.
509
 
    It is instantiated once, and simulates having a GnuTLS module."""
 
528
class gnutls:
 
529
    """This isn't so much a class as it is a module-like namespace."""
510
530
 
511
531
    library = ctypes.util.find_library("gnutls")
512
532
    if library is None:
513
533
        library = ctypes.util.find_library("gnutls-deb0")
514
534
    _library = ctypes.cdll.LoadLibrary(library)
515
535
    del library
516
 
    _need_version = b"3.3.0"
517
 
 
518
 
    def __init__(self):
519
 
        # Need to use "self" here, since this method is called before
520
 
        # the assignment to the "gnutls" global variable happens.
521
 
        if self.check_version(self._need_version) is None:
522
 
            raise self.Error("Needs GnuTLS {} or later"
523
 
                             .format(self._need_version))
524
536
 
525
537
    # Unless otherwise indicated, the constants and types below are
526
538
    # all from the gnutls/gnutls.h C header file.
530
542
    E_INTERRUPTED = -52
531
543
    E_AGAIN = -28
532
544
    CRT_OPENPGP = 2
 
545
    CRT_RAWPK = 3
533
546
    CLIENT = 2
534
547
    SHUT_RDWR = 0
535
548
    CRD_CERTIFICATE = 1
536
549
    E_NO_CERTIFICATE_FOUND = -49
 
550
    X509_FMT_DER = 0
 
551
    NO_TICKETS = 1<<10
 
552
    ENABLE_RAWPK = 1<<18
 
553
    CTYPE_PEERS = 3
 
554
    KEYID_USE_SHA256 = 1        # gnutls/x509.h
537
555
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
538
556
 
539
557
    # Types
562
580
 
563
581
    # Exceptions
564
582
    class Error(Exception):
565
 
        # We need to use the class name "GnuTLS" here, since this
566
 
        # exception might be raised from within GnuTLS.__init__,
567
 
        # which is called before the assignment to the "gnutls"
568
 
        # global variable has happened.
569
583
        def __init__(self, message=None, code=None, args=()):
570
584
            # Default usage is by a message string, but if a return
571
585
            # code is passed, convert it to a string with
572
586
            # gnutls.strerror()
573
587
            self.code = code
574
588
            if message is None and code is not None:
575
 
                message = GnuTLS.strerror(code)
576
 
            return super(GnuTLS.Error, self).__init__(
 
589
                message = gnutls.strerror(code)
 
590
            return super(gnutls.Error, self).__init__(
577
591
                message, *args)
578
592
 
579
593
    class CertificateSecurityError(Error):
580
594
        pass
581
595
 
582
596
    # Classes
583
 
    class Credentials(object):
 
597
    class Credentials:
584
598
        def __init__(self):
585
599
            self._c_object = gnutls.certificate_credentials_t()
586
600
            gnutls.certificate_allocate_credentials(
590
604
        def __del__(self):
591
605
            gnutls.certificate_free_credentials(self._c_object)
592
606
 
593
 
    class ClientSession(object):
 
607
    class ClientSession:
594
608
        def __init__(self, socket, credentials=None):
595
609
            self._c_object = gnutls.session_t()
596
 
            gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
 
610
            gnutls_flags = gnutls.CLIENT
 
611
            if gnutls.check_version(b"3.5.6"):
 
612
                gnutls_flags |= gnutls.NO_TICKETS
 
613
            if gnutls.has_rawpk:
 
614
                gnutls_flags |= gnutls.ENABLE_RAWPK
 
615
            gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
 
616
            del gnutls_flags
597
617
            gnutls.set_default_priority(self._c_object)
598
618
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
599
619
            gnutls.handshake_set_private_extensions(self._c_object,
731
751
    check_version.argtypes = [ctypes.c_char_p]
732
752
    check_version.restype = ctypes.c_char_p
733
753
 
734
 
    # All the function declarations below are from gnutls/openpgp.h
735
 
 
736
 
    openpgp_crt_init = _library.gnutls_openpgp_crt_init
737
 
    openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
738
 
    openpgp_crt_init.restype = _error_code
739
 
 
740
 
    openpgp_crt_import = _library.gnutls_openpgp_crt_import
741
 
    openpgp_crt_import.argtypes = [openpgp_crt_t,
742
 
                                   ctypes.POINTER(datum_t),
743
 
                                   openpgp_crt_fmt_t]
744
 
    openpgp_crt_import.restype = _error_code
745
 
 
746
 
    openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
747
 
    openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
748
 
                                        ctypes.POINTER(ctypes.c_uint)]
749
 
    openpgp_crt_verify_self.restype = _error_code
750
 
 
751
 
    openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
752
 
    openpgp_crt_deinit.argtypes = [openpgp_crt_t]
753
 
    openpgp_crt_deinit.restype = None
754
 
 
755
 
    openpgp_crt_get_fingerprint = (
756
 
        _library.gnutls_openpgp_crt_get_fingerprint)
757
 
    openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
758
 
                                            ctypes.c_void_p,
759
 
                                            ctypes.POINTER(
760
 
                                                ctypes.c_size_t)]
761
 
    openpgp_crt_get_fingerprint.restype = _error_code
 
754
    _need_version = b"3.3.0"
 
755
    if check_version(_need_version) is None:
 
756
        raise self.Error("Needs GnuTLS {} or later"
 
757
                         .format(_need_version))
 
758
 
 
759
    _tls_rawpk_version = b"3.6.6"
 
760
    has_rawpk = bool(check_version(_tls_rawpk_version))
 
761
 
 
762
    if has_rawpk:
 
763
        # Types
 
764
        class pubkey_st(ctypes.Structure):
 
765
            _fields = []
 
766
        pubkey_t = ctypes.POINTER(pubkey_st)
 
767
 
 
768
        x509_crt_fmt_t = ctypes.c_int
 
769
 
 
770
        # All the function declarations below are from gnutls/abstract.h
 
771
        pubkey_init = _library.gnutls_pubkey_init
 
772
        pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
 
773
        pubkey_init.restype = _error_code
 
774
 
 
775
        pubkey_import = _library.gnutls_pubkey_import
 
776
        pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
 
777
                                  x509_crt_fmt_t]
 
778
        pubkey_import.restype = _error_code
 
779
 
 
780
        pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
 
781
        pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
 
782
                                      ctypes.POINTER(ctypes.c_ubyte),
 
783
                                      ctypes.POINTER(ctypes.c_size_t)]
 
784
        pubkey_get_key_id.restype = _error_code
 
785
 
 
786
        pubkey_deinit = _library.gnutls_pubkey_deinit
 
787
        pubkey_deinit.argtypes = [pubkey_t]
 
788
        pubkey_deinit.restype = None
 
789
    else:
 
790
        # All the function declarations below are from gnutls/openpgp.h
 
791
 
 
792
        openpgp_crt_init = _library.gnutls_openpgp_crt_init
 
793
        openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
 
794
        openpgp_crt_init.restype = _error_code
 
795
 
 
796
        openpgp_crt_import = _library.gnutls_openpgp_crt_import
 
797
        openpgp_crt_import.argtypes = [openpgp_crt_t,
 
798
                                       ctypes.POINTER(datum_t),
 
799
                                       openpgp_crt_fmt_t]
 
800
        openpgp_crt_import.restype = _error_code
 
801
 
 
802
        openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
 
803
        openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
 
804
                                            ctypes.POINTER(ctypes.c_uint)]
 
805
        openpgp_crt_verify_self.restype = _error_code
 
806
 
 
807
        openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
 
808
        openpgp_crt_deinit.argtypes = [openpgp_crt_t]
 
809
        openpgp_crt_deinit.restype = None
 
810
 
 
811
        openpgp_crt_get_fingerprint = (
 
812
            _library.gnutls_openpgp_crt_get_fingerprint)
 
813
        openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
 
814
                                                ctypes.c_void_p,
 
815
                                                ctypes.POINTER(
 
816
                                                    ctypes.c_size_t)]
 
817
        openpgp_crt_get_fingerprint.restype = _error_code
 
818
 
 
819
    if check_version(b"3.6.4"):
 
820
        certificate_type_get2 = _library.gnutls_certificate_type_get2
 
821
        certificate_type_get2.argtypes = [session_t, ctypes.c_int]
 
822
        certificate_type_get2.restype = _error_code
762
823
 
763
824
    # Remove non-public functions
764
825
    del _error_code, _retry_on_error
765
 
# Create the global "gnutls" object, simulating a module
766
 
gnutls = GnuTLS()
767
826
 
768
827
 
769
828
def call_pipe(connection,       # : multiprocessing.Connection
777
836
    connection.close()
778
837
 
779
838
 
780
 
class Client(object):
 
839
class Client:
781
840
    """A representation of a client host served by this server.
782
841
 
783
842
    Attributes:
784
843
    approved:   bool(); 'None' if not yet approved/disapproved
785
844
    approval_delay: datetime.timedelta(); Time to wait for approval
786
845
    approval_duration: datetime.timedelta(); Duration of one approval
787
 
    checker:    subprocess.Popen(); a running checker process used
788
 
                                    to see if the client lives.
789
 
                                    'None' if no process is running.
 
846
    checker: multiprocessing.Process(); a running checker process used
 
847
             to see if the client lives. 'None' if no process is
 
848
             running.
790
849
    checker_callback_tag: a GLib event source tag, or None
791
850
    checker_command: string; External command which is run to check
792
851
                     if client lives.  %() expansions are done at
800
859
    disable_initiator_tag: a GLib event source tag, or None
801
860
    enabled:    bool()
802
861
    fingerprint: string (40 or 32 hexadecimal digits); used to
803
 
                 uniquely identify the client
 
862
                 uniquely identify an OpenPGP client
 
863
    key_id: string (64 hexadecimal digits); used to uniquely identify
 
864
            a client using raw public keys
804
865
    host:       string; available for use by the checker command
805
866
    interval:   datetime.timedelta(); How often to start a new checker
806
867
    last_approval_request: datetime.datetime(); (UTC) or None
824
885
    """
825
886
 
826
887
    runtime_expansions = ("approval_delay", "approval_duration",
827
 
                          "created", "enabled", "expires",
 
888
                          "created", "enabled", "expires", "key_id",
828
889
                          "fingerprint", "host", "interval",
829
890
                          "last_approval_request", "last_checked_ok",
830
891
                          "last_enabled", "name", "timeout")
860
921
            client["enabled"] = config.getboolean(client_name,
861
922
                                                  "enabled")
862
923
 
863
 
            # Uppercase and remove spaces from fingerprint for later
864
 
            # comparison purposes with return value from the
865
 
            # fingerprint() function
 
924
            # Uppercase and remove spaces from key_id and fingerprint
 
925
            # for later comparison purposes with return value from the
 
926
            # key_id() and fingerprint() functions
 
927
            client["key_id"] = (section.get("key_id", "").upper()
 
928
                                .replace(" ", ""))
866
929
            client["fingerprint"] = (section["fingerprint"].upper()
867
930
                                     .replace(" ", ""))
868
931
            if "secret" in section:
912
975
            self.expires = None
913
976
 
914
977
        logger.debug("Creating client %r", self.name)
 
978
        logger.debug("  Key ID: %s", self.key_id)
915
979
        logger.debug("  Fingerprint: %s", self.fingerprint)
916
980
        self.created = settings.get("created",
917
981
                                    datetime.datetime.utcnow())
981
1045
        if self.checker_initiator_tag is not None:
982
1046
            GLib.source_remove(self.checker_initiator_tag)
983
1047
        self.checker_initiator_tag = GLib.timeout_add(
984
 
            int(self.interval.total_seconds() * 1000),
 
1048
            random.randrange(int(self.interval.total_seconds() * 1000
 
1049
                                 + 1)),
985
1050
            self.start_checker)
986
1051
        # Schedule a disable() when 'timeout' has passed
987
1052
        if self.disable_initiator_tag is not None:
994
1059
    def checker_callback(self, source, condition, connection,
995
1060
                         command):
996
1061
        """The checker has completed, so take appropriate actions."""
997
 
        self.checker_callback_tag = None
998
 
        self.checker = None
999
1062
        # Read return code from connection (see call_pipe)
1000
1063
        returncode = connection.recv()
1001
1064
        connection.close()
 
1065
        if self.checker is not None:
 
1066
            self.checker.join()
 
1067
        self.checker_callback_tag = None
 
1068
        self.checker = None
1002
1069
 
1003
1070
        if returncode >= 0:
1004
1071
            self.last_checker_status = returncode
1093
1160
                kwargs=popen_args)
1094
1161
            self.checker.start()
1095
1162
            self.checker_callback_tag = GLib.io_add_watch(
1096
 
                pipe[0].fileno(), GLib.IO_IN,
 
1163
                GLib.IOChannel.unix_new(pipe[0].fileno()),
 
1164
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1097
1165
                self.checker_callback, pipe[0], command)
1098
1166
        # Re-run this periodically if run by GLib.timeout_add
1099
1167
        return True
1354
1422
                raise ValueError("Byte arrays not supported for non-"
1355
1423
                                 "'ay' signature {!r}"
1356
1424
                                 .format(prop._dbus_signature))
1357
 
            value = dbus.ByteArray(b''.join(chr(byte)
1358
 
                                            for byte in value))
 
1425
            value = dbus.ByteArray(bytes(value))
1359
1426
        prop(value)
1360
1427
 
1361
1428
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1999
2066
    def Name_dbus_property(self):
2000
2067
        return dbus.String(self.name)
2001
2068
 
 
2069
    # KeyID - property
 
2070
    @dbus_annotations(
 
2071
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
2072
    @dbus_service_property(_interface, signature="s", access="read")
 
2073
    def KeyID_dbus_property(self):
 
2074
        return dbus.String(self.key_id)
 
2075
 
2002
2076
    # Fingerprint - property
2003
2077
    @dbus_annotations(
2004
2078
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2159
2233
    del _interface
2160
2234
 
2161
2235
 
2162
 
class ProxyClient(object):
2163
 
    def __init__(self, child_pipe, fpr, address):
 
2236
class ProxyClient:
 
2237
    def __init__(self, child_pipe, key_id, fpr, address):
2164
2238
        self._pipe = child_pipe
2165
 
        self._pipe.send(('init', fpr, address))
 
2239
        self._pipe.send(('init', key_id, fpr, address))
2166
2240
        if not self._pipe.recv():
2167
 
            raise KeyError(fpr)
 
2241
            raise KeyError(key_id or fpr)
2168
2242
 
2169
2243
    def __getattribute__(self, name):
2170
2244
        if name == '_pipe':
2237
2311
 
2238
2312
            approval_required = False
2239
2313
            try:
2240
 
                try:
2241
 
                    fpr = self.fingerprint(
2242
 
                        self.peer_certificate(session))
2243
 
                except (TypeError, gnutls.Error) as error:
2244
 
                    logger.warning("Bad certificate: %s", error)
2245
 
                    return
2246
 
                logger.debug("Fingerprint: %s", fpr)
2247
 
 
2248
 
                try:
2249
 
                    client = ProxyClient(child_pipe, fpr,
 
2314
                if gnutls.has_rawpk:
 
2315
                    fpr = b""
 
2316
                    try:
 
2317
                        key_id = self.key_id(
 
2318
                            self.peer_certificate(session))
 
2319
                    except (TypeError, gnutls.Error) as error:
 
2320
                        logger.warning("Bad certificate: %s", error)
 
2321
                        return
 
2322
                    logger.debug("Key ID: %s", key_id)
 
2323
 
 
2324
                else:
 
2325
                    key_id = b""
 
2326
                    try:
 
2327
                        fpr = self.fingerprint(
 
2328
                            self.peer_certificate(session))
 
2329
                    except (TypeError, gnutls.Error) as error:
 
2330
                        logger.warning("Bad certificate: %s", error)
 
2331
                        return
 
2332
                    logger.debug("Fingerprint: %s", fpr)
 
2333
 
 
2334
                try:
 
2335
                    client = ProxyClient(child_pipe, key_id, fpr,
2250
2336
                                         self.client_address)
2251
2337
                except KeyError:
2252
2338
                    return
2329
2415
 
2330
2416
    @staticmethod
2331
2417
    def peer_certificate(session):
2332
 
        "Return the peer's OpenPGP certificate as a bytestring"
2333
 
        # If not an OpenPGP certificate...
2334
 
        if (gnutls.certificate_type_get(session._c_object)
2335
 
            != gnutls.CRT_OPENPGP):
 
2418
        "Return the peer's certificate as a bytestring"
 
2419
        try:
 
2420
            cert_type = gnutls.certificate_type_get2(session._c_object,
 
2421
                                                     gnutls.CTYPE_PEERS)
 
2422
        except AttributeError:
 
2423
            cert_type = gnutls.certificate_type_get(session._c_object)
 
2424
        if gnutls.has_rawpk:
 
2425
            valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
 
2426
        else:
 
2427
            valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
 
2428
        # If not a valid certificate type...
 
2429
        if cert_type not in valid_cert_types:
 
2430
            logger.info("Cert type %r not in %r", cert_type,
 
2431
                        valid_cert_types)
2336
2432
            # ...return invalid data
2337
2433
            return b""
2338
2434
        list_size = ctypes.c_uint(1)
2346
2442
        return ctypes.string_at(cert.data, cert.size)
2347
2443
 
2348
2444
    @staticmethod
 
2445
    def key_id(certificate):
 
2446
        "Convert a certificate bytestring to a hexdigit key ID"
 
2447
        # New GnuTLS "datum" with the public key
 
2448
        datum = gnutls.datum_t(
 
2449
            ctypes.cast(ctypes.c_char_p(certificate),
 
2450
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2451
            ctypes.c_uint(len(certificate)))
 
2452
        # XXX all these need to be created in the gnutls "module"
 
2453
        # New empty GnuTLS certificate
 
2454
        pubkey = gnutls.pubkey_t()
 
2455
        gnutls.pubkey_init(ctypes.byref(pubkey))
 
2456
        # Import the raw public key into the certificate
 
2457
        gnutls.pubkey_import(pubkey,
 
2458
                             ctypes.byref(datum),
 
2459
                             gnutls.X509_FMT_DER)
 
2460
        # New buffer for the key ID
 
2461
        buf = ctypes.create_string_buffer(32)
 
2462
        buf_len = ctypes.c_size_t(len(buf))
 
2463
        # Get the key ID from the raw public key into the buffer
 
2464
        gnutls.pubkey_get_key_id(pubkey,
 
2465
                                 gnutls.KEYID_USE_SHA256,
 
2466
                                 ctypes.cast(ctypes.byref(buf),
 
2467
                                             ctypes.POINTER(ctypes.c_ubyte)),
 
2468
                                 ctypes.byref(buf_len))
 
2469
        # Deinit the certificate
 
2470
        gnutls.pubkey_deinit(pubkey)
 
2471
 
 
2472
        # Convert the buffer to a Python bytestring
 
2473
        key_id = ctypes.string_at(buf, buf_len.value)
 
2474
        # Convert the bytestring to hexadecimal notation
 
2475
        hex_key_id = binascii.hexlify(key_id).upper()
 
2476
        return hex_key_id
 
2477
 
 
2478
    @staticmethod
2349
2479
    def fingerprint(openpgp):
2350
2480
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2351
2481
        # New GnuTLS "datum" with the OpenPGP public key
2365
2495
                                       ctypes.byref(crtverify))
2366
2496
        if crtverify.value != 0:
2367
2497
            gnutls.openpgp_crt_deinit(crt)
2368
 
            raise gnutls.CertificateSecurityError("Verify failed")
 
2498
            raise gnutls.CertificateSecurityError(code
 
2499
                                                  =crtverify.value)
2369
2500
        # New buffer for the fingerprint
2370
2501
        buf = ctypes.create_string_buffer(20)
2371
2502
        buf_len = ctypes.c_size_t()
2381
2512
        return hex_fpr
2382
2513
 
2383
2514
 
2384
 
class MultiprocessingMixIn(object):
 
2515
class MultiprocessingMixIn:
2385
2516
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2386
2517
 
2387
2518
    def sub_process_main(self, request, address):
2399
2530
        return proc
2400
2531
 
2401
2532
 
2402
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2533
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2403
2534
    """ adds a pipe to the MixIn """
2404
2535
 
2405
2536
    def process_request(self, request, client_address):
2420
2551
 
2421
2552
 
2422
2553
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2423
 
                     socketserver.TCPServer, object):
 
2554
                     socketserver.TCPServer):
2424
2555
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2425
2556
 
2426
2557
    Attributes:
2499
2630
                    raise
2500
2631
        # Only bind(2) the socket if we really need to.
2501
2632
        if self.server_address[0] or self.server_address[1]:
 
2633
            if self.server_address[1]:
 
2634
                self.allow_reuse_address = True
2502
2635
            if not self.server_address[0]:
2503
2636
                if self.address_family == socket.AF_INET6:
2504
2637
                    any_address = "::"  # in6addr_any
2557
2690
    def add_pipe(self, parent_pipe, proc):
2558
2691
        # Call "handle_ipc" for both data and EOF events
2559
2692
        GLib.io_add_watch(
2560
 
            parent_pipe.fileno(),
2561
 
            GLib.IO_IN | GLib.IO_HUP,
 
2693
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2694
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2562
2695
            functools.partial(self.handle_ipc,
2563
2696
                              parent_pipe=parent_pipe,
2564
2697
                              proc=proc))
2578
2711
        command = request[0]
2579
2712
 
2580
2713
        if command == 'init':
2581
 
            fpr = request[1]
2582
 
            address = request[2]
 
2714
            key_id = request[1].decode("ascii")
 
2715
            fpr = request[2].decode("ascii")
 
2716
            address = request[3]
2583
2717
 
2584
2718
            for c in self.clients.values():
2585
 
                if c.fingerprint == fpr:
 
2719
                if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
 
2720
                    continue
 
2721
                if key_id and c.key_id == key_id:
 
2722
                    client = c
 
2723
                    break
 
2724
                if fpr and c.fingerprint == fpr:
2586
2725
                    client = c
2587
2726
                    break
2588
2727
            else:
2589
 
                logger.info("Client not found for fingerprint: %s, ad"
2590
 
                            "dress: %s", fpr, address)
 
2728
                logger.info("Client not found for key ID: %s, address"
 
2729
                            ": %s", key_id or fpr, address)
2591
2730
                if self.use_dbus:
2592
2731
                    # Emit D-Bus signal
2593
 
                    mandos_dbus_service.ClientNotFound(fpr,
 
2732
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
2594
2733
                                                       address[0])
2595
2734
                parent_pipe.send(False)
2596
2735
                return False
2597
2736
 
2598
2737
            GLib.io_add_watch(
2599
 
                parent_pipe.fileno(),
2600
 
                GLib.IO_IN | GLib.IO_HUP,
 
2738
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2739
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2601
2740
                functools.partial(self.handle_ipc,
2602
2741
                                  parent_pipe=parent_pipe,
2603
2742
                                  proc=proc,
2618
2757
        if command == 'getattr':
2619
2758
            attrname = request[1]
2620
2759
            if isinstance(client_object.__getattribute__(attrname),
2621
 
                          collections.Callable):
 
2760
                          collections.abc.Callable):
2622
2761
                parent_pipe.send(('function', ))
2623
2762
            else:
2624
2763
                parent_pipe.send((
2635
2774
def rfc3339_duration_to_delta(duration):
2636
2775
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2637
2776
 
2638
 
    >>> rfc3339_duration_to_delta("P7D")
2639
 
    datetime.timedelta(7)
2640
 
    >>> rfc3339_duration_to_delta("PT60S")
2641
 
    datetime.timedelta(0, 60)
2642
 
    >>> rfc3339_duration_to_delta("PT60M")
2643
 
    datetime.timedelta(0, 3600)
2644
 
    >>> rfc3339_duration_to_delta("PT24H")
2645
 
    datetime.timedelta(1)
2646
 
    >>> rfc3339_duration_to_delta("P1W")
2647
 
    datetime.timedelta(7)
2648
 
    >>> rfc3339_duration_to_delta("PT5M30S")
2649
 
    datetime.timedelta(0, 330)
2650
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
2651
 
    datetime.timedelta(1, 200)
 
2777
    >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
 
2778
    True
 
2779
    >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
 
2780
    True
 
2781
    >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600)
 
2782
    True
 
2783
    >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
 
2784
    True
 
2785
    >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
 
2786
    True
 
2787
    >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
 
2788
    True
 
2789
    >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
 
2790
    True
2652
2791
    """
2653
2792
 
2654
2793
    # Parsing an RFC 3339 duration with regular expressions is not
2734
2873
def string_to_delta(interval):
2735
2874
    """Parse a string and return a datetime.timedelta
2736
2875
 
2737
 
    >>> string_to_delta('7d')
2738
 
    datetime.timedelta(7)
2739
 
    >>> string_to_delta('60s')
2740
 
    datetime.timedelta(0, 60)
2741
 
    >>> string_to_delta('60m')
2742
 
    datetime.timedelta(0, 3600)
2743
 
    >>> string_to_delta('24h')
2744
 
    datetime.timedelta(1)
2745
 
    >>> string_to_delta('1w')
2746
 
    datetime.timedelta(7)
2747
 
    >>> string_to_delta('5m 30s')
2748
 
    datetime.timedelta(0, 330)
 
2876
    >>> string_to_delta('7d') == datetime.timedelta(7)
 
2877
    True
 
2878
    >>> string_to_delta('60s') == datetime.timedelta(0, 60)
 
2879
    True
 
2880
    >>> string_to_delta('60m') == datetime.timedelta(0, 3600)
 
2881
    True
 
2882
    >>> string_to_delta('24h') == datetime.timedelta(1)
 
2883
    True
 
2884
    >>> string_to_delta('1w') == datetime.timedelta(7)
 
2885
    True
 
2886
    >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
 
2887
    True
2749
2888
    """
2750
2889
 
2751
2890
    try:
2853
2992
 
2854
2993
    options = parser.parse_args()
2855
2994
 
2856
 
    if options.check:
2857
 
        import doctest
2858
 
        fail_count, test_count = doctest.testmod()
2859
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2860
 
 
2861
2995
    # Default values for config file for server-global settings
 
2996
    if gnutls.has_rawpk:
 
2997
        priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
 
2998
                    ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
 
2999
    else:
 
3000
        priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
3001
                    ":+SIGN-DSA-SHA256")
2862
3002
    server_defaults = {"interface": "",
2863
3003
                       "address": "",
2864
3004
                       "port": "",
2865
3005
                       "debug": "False",
2866
 
                       "priority":
2867
 
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2868
 
                       ":+SIGN-DSA-SHA256",
 
3006
                       "priority": priority,
2869
3007
                       "servicename": "Mandos",
2870
3008
                       "use_dbus": "True",
2871
3009
                       "use_ipv6": "True",
2876
3014
                       "foreground": "False",
2877
3015
                       "zeroconf": "True",
2878
3016
                       }
 
3017
    del priority
2879
3018
 
2880
3019
    # Parse config file for server-global settings
2881
 
    server_config = configparser.SafeConfigParser(server_defaults)
 
3020
    server_config = configparser.ConfigParser(server_defaults)
2882
3021
    del server_defaults
2883
3022
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
2884
 
    # Convert the SafeConfigParser object to a dict
 
3023
    # Convert the ConfigParser object to a dict
2885
3024
    server_settings = server_config.defaults()
2886
3025
    # Use the appropriate methods on the non-string config options
2887
3026
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2959
3098
                                  server_settings["servicename"])))
2960
3099
 
2961
3100
    # Parse config file with clients
2962
 
    client_config = configparser.SafeConfigParser(Client
2963
 
                                                  .client_defaults)
 
3101
    client_config = configparser.ConfigParser(Client.client_defaults)
2964
3102
    client_config.read(os.path.join(server_settings["configdir"],
2965
3103
                                    "clients.conf"))
2966
3104
 
3037
3175
        # Close all input and output, do double fork, etc.
3038
3176
        daemon()
3039
3177
 
3040
 
    # multiprocessing will use threads, so before we use GLib we need
3041
 
    # to inform GLib that threads will be used.
3042
 
    GLib.threads_init()
 
3178
    if gi.version_info < (3, 10, 2):
 
3179
        # multiprocessing will use threads, so before we use GLib we
 
3180
        # need to inform GLib that threads will be used.
 
3181
        GLib.threads_init()
3043
3182
 
3044
3183
    global main_loop
3045
3184
    # From the Avahi example code
3121
3260
                             if isinstance(s, bytes)
3122
3261
                             else s) for s in
3123
3262
                            value["client_structure"]]
3124
 
                        # .name & .host
3125
 
                        for k in ("name", "host"):
 
3263
                        # .name, .host, and .checker_command
 
3264
                        for k in ("name", "host", "checker_command"):
3126
3265
                            if isinstance(value[k], bytes):
3127
3266
                                value[k] = value[k].decode("utf-8")
 
3267
                        if "key_id" not in value:
 
3268
                            value["key_id"] = ""
 
3269
                        elif "fingerprint" not in value:
 
3270
                            value["fingerprint"] = ""
3128
3271
                    #  old_client_settings
3129
3272
                    # .keys()
3130
3273
                    old_client_settings = {
3134
3277
                        for key, value in
3135
3278
                        bytes_old_client_settings.items()}
3136
3279
                    del bytes_old_client_settings
3137
 
                    # .host
 
3280
                    # .host and .checker_command
3138
3281
                    for value in old_client_settings.values():
3139
 
                        if isinstance(value["host"], bytes):
3140
 
                            value["host"] = (value["host"]
3141
 
                                             .decode("utf-8"))
 
3282
                        for attribute in ("host", "checker_command"):
 
3283
                            if isinstance(value[attribute], bytes):
 
3284
                                value[attribute] = (value[attribute]
 
3285
                                                    .decode("utf-8"))
3142
3286
            os.remove(stored_state_path)
3143
3287
        except IOError as e:
3144
3288
            if e.errno == errno.ENOENT:
3267
3411
                pass
3268
3412
 
3269
3413
            @dbus.service.signal(_interface, signature="ss")
3270
 
            def ClientNotFound(self, fingerprint, address):
 
3414
            def ClientNotFound(self, key_id, address):
3271
3415
                "D-Bus signal"
3272
3416
                pass
3273
3417
 
3469
3613
                sys.exit(1)
3470
3614
            # End of Avahi example code
3471
3615
 
3472
 
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3473
 
                          lambda *args, **kwargs:
3474
 
                          (tcp_server.handle_request
3475
 
                           (*args[2:], **kwargs) or True))
 
3616
        GLib.io_add_watch(
 
3617
            GLib.IOChannel.unix_new(tcp_server.fileno()),
 
3618
            GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
3619
            lambda *args, **kwargs: (tcp_server.handle_request
 
3620
                                     (*args[2:], **kwargs) or True))
3476
3621
 
3477
3622
        logger.debug("Starting main loop")
3478
3623
        main_loop.run()
3488
3633
    # Must run before the D-Bus bus name gets deregistered
3489
3634
    cleanup()
3490
3635
 
 
3636
 
 
3637
def should_only_run_tests():
 
3638
    parser = argparse.ArgumentParser(add_help=False)
 
3639
    parser.add_argument("--check", action='store_true')
 
3640
    args, unknown_args = parser.parse_known_args()
 
3641
    run_tests = args.check
 
3642
    if run_tests:
 
3643
        # Remove --check argument from sys.argv
 
3644
        sys.argv[1:] = unknown_args
 
3645
    return run_tests
 
3646
 
 
3647
# Add all tests from doctest strings
 
3648
def load_tests(loader, tests, none):
 
3649
    import doctest
 
3650
    tests.addTests(doctest.DocTestSuite())
 
3651
    return tests
3491
3652
 
3492
3653
if __name__ == '__main__':
3493
 
    main()
 
3654
    try:
 
3655
        if should_only_run_tests():
 
3656
            # Call using ./mandos --check [--verbose]
 
3657
            unittest.main()
 
3658
        else:
 
3659
            main()
 
3660
    finally:
 
3661
        logging.shutdown()