/mandos/release

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2021-02-03 08:57:27 UTC
  • Revision ID: teddy@recompile.se-20210203085727-r2nseei71wb0clpf
Tags: version-1.8.14-1
Version 1.8.14-1

* Makefile (version): Change to "1.8.14".
* NEWS (Version 1.8.14): Add new entry.
* debian/changelog (1.8.14-1): - '' -

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-2018 Teddy Hogeborn
15
 
# Copyright © 2008-2018 Björn Påhlsson
 
14
# Copyright © 2008-2020 Teddy Hogeborn
 
15
# Copyright © 2008-2020 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
 
82
import shlex
80
83
 
81
84
import dbus
82
85
import dbus.service
 
86
import gi
83
87
from gi.repository import GLib
84
88
from dbus.mainloop.glib import DBusGMainLoop
85
89
import ctypes
87
91
import xml.dom.minidom
88
92
import inspect
89
93
 
 
94
if sys.version_info.major == 2:
 
95
    __metaclass__ = type
 
96
    str = unicode
 
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
# Show warnings by default
 
114
if not sys.warnoptions:
 
115
    import warnings
 
116
    warnings.simplefilter("default")
 
117
 
90
118
# Try to find the value of SO_BINDTODEVICE:
91
119
try:
92
120
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
112
140
            # No value found
113
141
            SO_BINDTODEVICE = None
114
142
 
115
 
if sys.version_info.major == 2:
116
 
    str = unicode
 
143
if sys.version_info < (3, 2):
 
144
    configparser.Configparser = configparser.SafeConfigParser
117
145
 
118
 
version = "1.7.19"
 
146
version = "1.8.14"
119
147
stored_state_file = "clients.pickle"
120
148
 
121
149
logger = logging.getLogger()
 
150
logging.captureWarnings(True)   # Show warnings via the logging system
122
151
syslogger = None
123
152
 
124
153
try:
179
208
    pass
180
209
 
181
210
 
182
 
class PGPEngine(object):
 
211
class PGPEngine:
183
212
    """A simple class for OpenPGP symmetric encryption & decryption"""
184
213
 
185
214
    def __init__(self):
189
218
            output = subprocess.check_output(["gpgconf"])
190
219
            for line in output.splitlines():
191
220
                name, text, path = line.split(b":")
192
 
                if name == "gpg":
 
221
                if name == b"gpg":
193
222
                    self.gpg = path
194
223
                    break
195
224
        except OSError as e:
200
229
                          '--force-mdc',
201
230
                          '--quiet']
202
231
        # Only GPG version 1 has the --no-use-agent option.
203
 
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
 
232
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
204
233
            self.gnupgargs.append("--no-use-agent")
205
234
 
206
235
    def __enter__(self):
275
304
 
276
305
 
277
306
# 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."""
 
307
class avahi:
 
308
    """This isn't so much a class as it is a module-like namespace."""
281
309
    IF_UNSPEC = -1               # avahi-common/address.h
282
310
    PROTO_UNSPEC = -1            # avahi-common/address.h
283
311
    PROTO_INET = 0               # avahi-common/address.h
287
315
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
288
316
    DBUS_PATH_SERVER = "/"
289
317
 
290
 
    def string_array_to_txt_array(self, t):
 
318
    @staticmethod
 
319
    def string_array_to_txt_array(t):
291
320
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
292
321
                           for s in t), signature="ay")
293
322
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
298
327
    SERVER_RUNNING = 2           # avahi-common/defs.h
299
328
    SERVER_COLLISION = 3         # avahi-common/defs.h
300
329
    SERVER_FAILURE = 4           # avahi-common/defs.h
301
 
avahi = Avahi()
302
330
 
303
331
 
304
332
class AvahiError(Exception):
316
344
    pass
317
345
 
318
346
 
319
 
class AvahiService(object):
 
347
class AvahiService:
320
348
    """An Avahi (Zeroconf) service.
321
349
 
322
350
    Attributes:
496
524
class AvahiServiceToSyslog(AvahiService):
497
525
    def rename(self, *args, **kwargs):
498
526
        """Add the new name to the syslog messages"""
499
 
        ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
 
527
        ret = super(AvahiServiceToSyslog, self).rename(*args,
 
528
                                                       **kwargs)
500
529
        syslogger.setFormatter(logging.Formatter(
501
530
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
502
531
            .format(self.name)))
504
533
 
505
534
 
506
535
# 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."""
 
536
class gnutls:
 
537
    """This isn't so much a class as it is a module-like namespace."""
510
538
 
511
539
    library = ctypes.util.find_library("gnutls")
512
540
    if library is None:
513
541
        library = ctypes.util.find_library("gnutls-deb0")
514
542
    _library = ctypes.cdll.LoadLibrary(library)
515
543
    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
544
 
525
545
    # Unless otherwise indicated, the constants and types below are
526
546
    # all from the gnutls/gnutls.h C header file.
530
550
    E_INTERRUPTED = -52
531
551
    E_AGAIN = -28
532
552
    CRT_OPENPGP = 2
 
553
    CRT_RAWPK = 3
533
554
    CLIENT = 2
534
555
    SHUT_RDWR = 0
535
556
    CRD_CERTIFICATE = 1
536
557
    E_NO_CERTIFICATE_FOUND = -49
 
558
    X509_FMT_DER = 0
 
559
    NO_TICKETS = 1<<10
 
560
    ENABLE_RAWPK = 1<<18
 
561
    CTYPE_PEERS = 3
 
562
    KEYID_USE_SHA256 = 1        # gnutls/x509.h
537
563
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
538
564
 
539
565
    # Types
562
588
 
563
589
    # Exceptions
564
590
    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
591
        def __init__(self, message=None, code=None, args=()):
570
592
            # Default usage is by a message string, but if a return
571
593
            # code is passed, convert it to a string with
572
594
            # gnutls.strerror()
573
595
            self.code = code
574
596
            if message is None and code is not None:
575
 
                message = GnuTLS.strerror(code)
576
 
            return super(GnuTLS.Error, self).__init__(
 
597
                message = gnutls.strerror(code)
 
598
            return super(gnutls.Error, self).__init__(
577
599
                message, *args)
578
600
 
579
601
    class CertificateSecurityError(Error):
580
602
        pass
581
603
 
582
604
    # Classes
583
 
    class Credentials(object):
 
605
    class Credentials:
584
606
        def __init__(self):
585
607
            self._c_object = gnutls.certificate_credentials_t()
586
608
            gnutls.certificate_allocate_credentials(
590
612
        def __del__(self):
591
613
            gnutls.certificate_free_credentials(self._c_object)
592
614
 
593
 
    class ClientSession(object):
 
615
    class ClientSession:
594
616
        def __init__(self, socket, credentials=None):
595
617
            self._c_object = gnutls.session_t()
596
 
            gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
 
618
            gnutls_flags = gnutls.CLIENT
 
619
            if gnutls.check_version(b"3.5.6"):
 
620
                gnutls_flags |= gnutls.NO_TICKETS
 
621
            if gnutls.has_rawpk:
 
622
                gnutls_flags |= gnutls.ENABLE_RAWPK
 
623
            gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
 
624
            del gnutls_flags
597
625
            gnutls.set_default_priority(self._c_object)
598
626
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
599
627
            gnutls.handshake_set_private_extensions(self._c_object,
731
759
    check_version.argtypes = [ctypes.c_char_p]
732
760
    check_version.restype = ctypes.c_char_p
733
761
 
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
 
762
    _need_version = b"3.3.0"
 
763
    if check_version(_need_version) is None:
 
764
        raise self.Error("Needs GnuTLS {} or later"
 
765
                         .format(_need_version))
 
766
 
 
767
    _tls_rawpk_version = b"3.6.6"
 
768
    has_rawpk = bool(check_version(_tls_rawpk_version))
 
769
 
 
770
    if has_rawpk:
 
771
        # Types
 
772
        class pubkey_st(ctypes.Structure):
 
773
            _fields = []
 
774
        pubkey_t = ctypes.POINTER(pubkey_st)
 
775
 
 
776
        x509_crt_fmt_t = ctypes.c_int
 
777
 
 
778
        # All the function declarations below are from
 
779
        # gnutls/abstract.h
 
780
        pubkey_init = _library.gnutls_pubkey_init
 
781
        pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
 
782
        pubkey_init.restype = _error_code
 
783
 
 
784
        pubkey_import = _library.gnutls_pubkey_import
 
785
        pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
 
786
                                  x509_crt_fmt_t]
 
787
        pubkey_import.restype = _error_code
 
788
 
 
789
        pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
 
790
        pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
 
791
                                      ctypes.POINTER(ctypes.c_ubyte),
 
792
                                      ctypes.POINTER(ctypes.c_size_t)]
 
793
        pubkey_get_key_id.restype = _error_code
 
794
 
 
795
        pubkey_deinit = _library.gnutls_pubkey_deinit
 
796
        pubkey_deinit.argtypes = [pubkey_t]
 
797
        pubkey_deinit.restype = None
 
798
    else:
 
799
        # All the function declarations below are from
 
800
        # gnutls/openpgp.h
 
801
 
 
802
        openpgp_crt_init = _library.gnutls_openpgp_crt_init
 
803
        openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
 
804
        openpgp_crt_init.restype = _error_code
 
805
 
 
806
        openpgp_crt_import = _library.gnutls_openpgp_crt_import
 
807
        openpgp_crt_import.argtypes = [openpgp_crt_t,
 
808
                                       ctypes.POINTER(datum_t),
 
809
                                       openpgp_crt_fmt_t]
 
810
        openpgp_crt_import.restype = _error_code
 
811
 
 
812
        openpgp_crt_verify_self = \
 
813
            _library.gnutls_openpgp_crt_verify_self
 
814
        openpgp_crt_verify_self.argtypes = [
 
815
            openpgp_crt_t,
 
816
            ctypes.c_uint,
 
817
            ctypes.POINTER(ctypes.c_uint),
 
818
        ]
 
819
        openpgp_crt_verify_self.restype = _error_code
 
820
 
 
821
        openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
 
822
        openpgp_crt_deinit.argtypes = [openpgp_crt_t]
 
823
        openpgp_crt_deinit.restype = None
 
824
 
 
825
        openpgp_crt_get_fingerprint = (
 
826
            _library.gnutls_openpgp_crt_get_fingerprint)
 
827
        openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
 
828
                                                ctypes.c_void_p,
 
829
                                                ctypes.POINTER(
 
830
                                                    ctypes.c_size_t)]
 
831
        openpgp_crt_get_fingerprint.restype = _error_code
 
832
 
 
833
    if check_version(b"3.6.4"):
 
834
        certificate_type_get2 = _library.gnutls_certificate_type_get2
 
835
        certificate_type_get2.argtypes = [session_t, ctypes.c_int]
 
836
        certificate_type_get2.restype = _error_code
762
837
 
763
838
    # Remove non-public functions
764
839
    del _error_code, _retry_on_error
765
 
# Create the global "gnutls" object, simulating a module
766
 
gnutls = GnuTLS()
767
840
 
768
841
 
769
842
def call_pipe(connection,       # : multiprocessing.Connection
777
850
    connection.close()
778
851
 
779
852
 
780
 
class Client(object):
 
853
class Client:
781
854
    """A representation of a client host served by this server.
782
855
 
783
856
    Attributes:
784
857
    approved:   bool(); 'None' if not yet approved/disapproved
785
858
    approval_delay: datetime.timedelta(); Time to wait for approval
786
859
    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.
 
860
    checker: multiprocessing.Process(); a running checker process used
 
861
             to see if the client lives. 'None' if no process is
 
862
             running.
790
863
    checker_callback_tag: a GLib event source tag, or None
791
864
    checker_command: string; External command which is run to check
792
865
                     if client lives.  %() expansions are done at
800
873
    disable_initiator_tag: a GLib event source tag, or None
801
874
    enabled:    bool()
802
875
    fingerprint: string (40 or 32 hexadecimal digits); used to
803
 
                 uniquely identify the client
 
876
                 uniquely identify an OpenPGP client
 
877
    key_id: string (64 hexadecimal digits); used to uniquely identify
 
878
            a client using raw public keys
804
879
    host:       string; available for use by the checker command
805
880
    interval:   datetime.timedelta(); How often to start a new checker
806
881
    last_approval_request: datetime.datetime(); (UTC) or None
824
899
    """
825
900
 
826
901
    runtime_expansions = ("approval_delay", "approval_duration",
827
 
                          "created", "enabled", "expires",
 
902
                          "created", "enabled", "expires", "key_id",
828
903
                          "fingerprint", "host", "interval",
829
904
                          "last_approval_request", "last_checked_ok",
830
905
                          "last_enabled", "name", "timeout")
860
935
            client["enabled"] = config.getboolean(client_name,
861
936
                                                  "enabled")
862
937
 
863
 
            # Uppercase and remove spaces from fingerprint for later
864
 
            # comparison purposes with return value from the
865
 
            # fingerprint() function
 
938
            # Uppercase and remove spaces from key_id and fingerprint
 
939
            # for later comparison purposes with return value from the
 
940
            # key_id() and fingerprint() functions
 
941
            client["key_id"] = (section.get("key_id", "").upper()
 
942
                                .replace(" ", ""))
866
943
            client["fingerprint"] = (section["fingerprint"].upper()
867
944
                                     .replace(" ", ""))
868
945
            if "secret" in section:
912
989
            self.expires = None
913
990
 
914
991
        logger.debug("Creating client %r", self.name)
 
992
        logger.debug("  Key ID: %s", self.key_id)
915
993
        logger.debug("  Fingerprint: %s", self.fingerprint)
916
994
        self.created = settings.get("created",
917
995
                                    datetime.datetime.utcnow())
981
1059
        if self.checker_initiator_tag is not None:
982
1060
            GLib.source_remove(self.checker_initiator_tag)
983
1061
        self.checker_initiator_tag = GLib.timeout_add(
984
 
            int(self.interval.total_seconds() * 1000),
 
1062
            random.randrange(int(self.interval.total_seconds() * 1000
 
1063
                                 + 1)),
985
1064
            self.start_checker)
986
1065
        # Schedule a disable() when 'timeout' has passed
987
1066
        if self.disable_initiator_tag is not None:
994
1073
    def checker_callback(self, source, condition, connection,
995
1074
                         command):
996
1075
        """The checker has completed, so take appropriate actions."""
997
 
        self.checker_callback_tag = None
998
 
        self.checker = None
999
1076
        # Read return code from connection (see call_pipe)
1000
1077
        returncode = connection.recv()
1001
1078
        connection.close()
 
1079
        if self.checker is not None:
 
1080
            self.checker.join()
 
1081
        self.checker_callback_tag = None
 
1082
        self.checker = None
1002
1083
 
1003
1084
        if returncode >= 0:
1004
1085
            self.last_checker_status = returncode
1060
1141
        if self.checker is None:
1061
1142
            # Escape attributes for the shell
1062
1143
            escaped_attrs = {
1063
 
                attr: re.escape(str(getattr(self, attr)))
 
1144
                attr: shlex.quote(str(getattr(self, attr)))
1064
1145
                for attr in self.runtime_expansions}
1065
1146
            try:
1066
1147
                command = self.checker_command % escaped_attrs
1093
1174
                kwargs=popen_args)
1094
1175
            self.checker.start()
1095
1176
            self.checker_callback_tag = GLib.io_add_watch(
1096
 
                pipe[0].fileno(), GLib.IO_IN,
 
1177
                GLib.IOChannel.unix_new(pipe[0].fileno()),
 
1178
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1097
1179
                self.checker_callback, pipe[0], command)
1098
1180
        # Re-run this periodically if run by GLib.timeout_add
1099
1181
        return True
1354
1436
                raise ValueError("Byte arrays not supported for non-"
1355
1437
                                 "'ay' signature {!r}"
1356
1438
                                 .format(prop._dbus_signature))
1357
 
            value = dbus.ByteArray(b''.join(chr(byte)
1358
 
                                            for byte in value))
 
1439
            value = dbus.ByteArray(bytes(value))
1359
1440
        prop(value)
1360
1441
 
1361
1442
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1999
2080
    def Name_dbus_property(self):
2000
2081
        return dbus.String(self.name)
2001
2082
 
 
2083
    # KeyID - property
 
2084
    @dbus_annotations(
 
2085
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
 
2086
    @dbus_service_property(_interface, signature="s", access="read")
 
2087
    def KeyID_dbus_property(self):
 
2088
        return dbus.String(self.key_id)
 
2089
 
2002
2090
    # Fingerprint - property
2003
2091
    @dbus_annotations(
2004
2092
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2159
2247
    del _interface
2160
2248
 
2161
2249
 
2162
 
class ProxyClient(object):
2163
 
    def __init__(self, child_pipe, fpr, address):
 
2250
class ProxyClient:
 
2251
    def __init__(self, child_pipe, key_id, fpr, address):
2164
2252
        self._pipe = child_pipe
2165
 
        self._pipe.send(('init', fpr, address))
 
2253
        self._pipe.send(('init', key_id, fpr, address))
2166
2254
        if not self._pipe.recv():
2167
 
            raise KeyError(fpr)
 
2255
            raise KeyError(key_id or fpr)
2168
2256
 
2169
2257
    def __getattribute__(self, name):
2170
2258
        if name == '_pipe':
2237
2325
 
2238
2326
            approval_required = False
2239
2327
            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,
 
2328
                if gnutls.has_rawpk:
 
2329
                    fpr = b""
 
2330
                    try:
 
2331
                        key_id = self.key_id(
 
2332
                            self.peer_certificate(session))
 
2333
                    except (TypeError, gnutls.Error) as error:
 
2334
                        logger.warning("Bad certificate: %s", error)
 
2335
                        return
 
2336
                    logger.debug("Key ID: %s", key_id)
 
2337
 
 
2338
                else:
 
2339
                    key_id = b""
 
2340
                    try:
 
2341
                        fpr = self.fingerprint(
 
2342
                            self.peer_certificate(session))
 
2343
                    except (TypeError, gnutls.Error) as error:
 
2344
                        logger.warning("Bad certificate: %s", error)
 
2345
                        return
 
2346
                    logger.debug("Fingerprint: %s", fpr)
 
2347
 
 
2348
                try:
 
2349
                    client = ProxyClient(child_pipe, key_id, fpr,
2250
2350
                                         self.client_address)
2251
2351
                except KeyError:
2252
2352
                    return
2329
2429
 
2330
2430
    @staticmethod
2331
2431
    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):
 
2432
        "Return the peer's certificate as a bytestring"
 
2433
        try:
 
2434
            cert_type = gnutls.certificate_type_get2(session._c_object,
 
2435
                                                     gnutls.CTYPE_PEERS)
 
2436
        except AttributeError:
 
2437
            cert_type = gnutls.certificate_type_get(session._c_object)
 
2438
        if gnutls.has_rawpk:
 
2439
            valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
 
2440
        else:
 
2441
            valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
 
2442
        # If not a valid certificate type...
 
2443
        if cert_type not in valid_cert_types:
 
2444
            logger.info("Cert type %r not in %r", cert_type,
 
2445
                        valid_cert_types)
2336
2446
            # ...return invalid data
2337
2447
            return b""
2338
2448
        list_size = ctypes.c_uint(1)
2346
2456
        return ctypes.string_at(cert.data, cert.size)
2347
2457
 
2348
2458
    @staticmethod
 
2459
    def key_id(certificate):
 
2460
        "Convert a certificate bytestring to a hexdigit key ID"
 
2461
        # New GnuTLS "datum" with the public key
 
2462
        datum = gnutls.datum_t(
 
2463
            ctypes.cast(ctypes.c_char_p(certificate),
 
2464
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2465
            ctypes.c_uint(len(certificate)))
 
2466
        # XXX all these need to be created in the gnutls "module"
 
2467
        # New empty GnuTLS certificate
 
2468
        pubkey = gnutls.pubkey_t()
 
2469
        gnutls.pubkey_init(ctypes.byref(pubkey))
 
2470
        # Import the raw public key into the certificate
 
2471
        gnutls.pubkey_import(pubkey,
 
2472
                             ctypes.byref(datum),
 
2473
                             gnutls.X509_FMT_DER)
 
2474
        # New buffer for the key ID
 
2475
        buf = ctypes.create_string_buffer(32)
 
2476
        buf_len = ctypes.c_size_t(len(buf))
 
2477
        # Get the key ID from the raw public key into the buffer
 
2478
        gnutls.pubkey_get_key_id(
 
2479
            pubkey,
 
2480
            gnutls.KEYID_USE_SHA256,
 
2481
            ctypes.cast(ctypes.byref(buf),
 
2482
                        ctypes.POINTER(ctypes.c_ubyte)),
 
2483
            ctypes.byref(buf_len))
 
2484
        # Deinit the certificate
 
2485
        gnutls.pubkey_deinit(pubkey)
 
2486
 
 
2487
        # Convert the buffer to a Python bytestring
 
2488
        key_id = ctypes.string_at(buf, buf_len.value)
 
2489
        # Convert the bytestring to hexadecimal notation
 
2490
        hex_key_id = binascii.hexlify(key_id).upper()
 
2491
        return hex_key_id
 
2492
 
 
2493
    @staticmethod
2349
2494
    def fingerprint(openpgp):
2350
2495
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2351
2496
        # New GnuTLS "datum" with the OpenPGP public key
2382
2527
        return hex_fpr
2383
2528
 
2384
2529
 
2385
 
class MultiprocessingMixIn(object):
 
2530
class MultiprocessingMixIn:
2386
2531
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2387
2532
 
2388
2533
    def sub_process_main(self, request, address):
2400
2545
        return proc
2401
2546
 
2402
2547
 
2403
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2548
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2404
2549
    """ adds a pipe to the MixIn """
2405
2550
 
2406
2551
    def process_request(self, request, client_address):
2421
2566
 
2422
2567
 
2423
2568
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2424
 
                     socketserver.TCPServer, object):
 
2569
                     socketserver.TCPServer):
2425
2570
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2426
2571
 
2427
2572
    Attributes:
2500
2645
                    raise
2501
2646
        # Only bind(2) the socket if we really need to.
2502
2647
        if self.server_address[0] or self.server_address[1]:
 
2648
            if self.server_address[1]:
 
2649
                self.allow_reuse_address = True
2503
2650
            if not self.server_address[0]:
2504
2651
                if self.address_family == socket.AF_INET6:
2505
2652
                    any_address = "::"  # in6addr_any
2558
2705
    def add_pipe(self, parent_pipe, proc):
2559
2706
        # Call "handle_ipc" for both data and EOF events
2560
2707
        GLib.io_add_watch(
2561
 
            parent_pipe.fileno(),
2562
 
            GLib.IO_IN | GLib.IO_HUP,
 
2708
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2709
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2563
2710
            functools.partial(self.handle_ipc,
2564
2711
                              parent_pipe=parent_pipe,
2565
2712
                              proc=proc))
2579
2726
        command = request[0]
2580
2727
 
2581
2728
        if command == 'init':
2582
 
            fpr = request[1].decode("ascii")
2583
 
            address = request[2]
 
2729
            key_id = request[1].decode("ascii")
 
2730
            fpr = request[2].decode("ascii")
 
2731
            address = request[3]
2584
2732
 
2585
2733
            for c in self.clients.values():
2586
 
                if c.fingerprint == fpr:
 
2734
                if key_id == ("E3B0C44298FC1C149AFBF4C8996FB924"
 
2735
                              "27AE41E4649B934CA495991B7852B855"):
 
2736
                    continue
 
2737
                if key_id and c.key_id == key_id:
 
2738
                    client = c
 
2739
                    break
 
2740
                if fpr and c.fingerprint == fpr:
2587
2741
                    client = c
2588
2742
                    break
2589
2743
            else:
2590
 
                logger.info("Client not found for fingerprint: %s, ad"
2591
 
                            "dress: %s", fpr, address)
 
2744
                logger.info("Client not found for key ID: %s, address"
 
2745
                            ": %s", key_id or fpr, address)
2592
2746
                if self.use_dbus:
2593
2747
                    # Emit D-Bus signal
2594
 
                    mandos_dbus_service.ClientNotFound(fpr,
 
2748
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
2595
2749
                                                       address[0])
2596
2750
                parent_pipe.send(False)
2597
2751
                return False
2598
2752
 
2599
2753
            GLib.io_add_watch(
2600
 
                parent_pipe.fileno(),
2601
 
                GLib.IO_IN | GLib.IO_HUP,
 
2754
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2755
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2602
2756
                functools.partial(self.handle_ipc,
2603
2757
                                  parent_pipe=parent_pipe,
2604
2758
                                  proc=proc,
2619
2773
        if command == 'getattr':
2620
2774
            attrname = request[1]
2621
2775
            if isinstance(client_object.__getattribute__(attrname),
2622
 
                          collections.Callable):
 
2776
                          collections.abc.Callable):
2623
2777
                parent_pipe.send(('function', ))
2624
2778
            else:
2625
2779
                parent_pipe.send((
2636
2790
def rfc3339_duration_to_delta(duration):
2637
2791
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2638
2792
 
2639
 
    >>> rfc3339_duration_to_delta("P7D")
2640
 
    datetime.timedelta(7)
2641
 
    >>> rfc3339_duration_to_delta("PT60S")
2642
 
    datetime.timedelta(0, 60)
2643
 
    >>> rfc3339_duration_to_delta("PT60M")
2644
 
    datetime.timedelta(0, 3600)
2645
 
    >>> rfc3339_duration_to_delta("PT24H")
2646
 
    datetime.timedelta(1)
2647
 
    >>> rfc3339_duration_to_delta("P1W")
2648
 
    datetime.timedelta(7)
2649
 
    >>> rfc3339_duration_to_delta("PT5M30S")
2650
 
    datetime.timedelta(0, 330)
2651
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
2652
 
    datetime.timedelta(1, 200)
 
2793
    >>> timedelta = datetime.timedelta
 
2794
    >>> rfc3339_duration_to_delta("P7D") == timedelta(7)
 
2795
    True
 
2796
    >>> rfc3339_duration_to_delta("PT60S") == timedelta(0, 60)
 
2797
    True
 
2798
    >>> rfc3339_duration_to_delta("PT60M") == timedelta(0, 3600)
 
2799
    True
 
2800
    >>> rfc3339_duration_to_delta("PT24H") == timedelta(1)
 
2801
    True
 
2802
    >>> rfc3339_duration_to_delta("P1W") == timedelta(7)
 
2803
    True
 
2804
    >>> rfc3339_duration_to_delta("PT5M30S") == timedelta(0, 330)
 
2805
    True
 
2806
    >>> rfc3339_duration_to_delta("P1DT3M20S") == timedelta(1, 200)
 
2807
    True
 
2808
    >>> del timedelta
2653
2809
    """
2654
2810
 
2655
2811
    # Parsing an RFC 3339 duration with regular expressions is not
2735
2891
def string_to_delta(interval):
2736
2892
    """Parse a string and return a datetime.timedelta
2737
2893
 
2738
 
    >>> string_to_delta('7d')
2739
 
    datetime.timedelta(7)
2740
 
    >>> string_to_delta('60s')
2741
 
    datetime.timedelta(0, 60)
2742
 
    >>> string_to_delta('60m')
2743
 
    datetime.timedelta(0, 3600)
2744
 
    >>> string_to_delta('24h')
2745
 
    datetime.timedelta(1)
2746
 
    >>> string_to_delta('1w')
2747
 
    datetime.timedelta(7)
2748
 
    >>> string_to_delta('5m 30s')
2749
 
    datetime.timedelta(0, 330)
 
2894
    >>> string_to_delta('7d') == datetime.timedelta(7)
 
2895
    True
 
2896
    >>> string_to_delta('60s') == datetime.timedelta(0, 60)
 
2897
    True
 
2898
    >>> string_to_delta('60m') == datetime.timedelta(0, 3600)
 
2899
    True
 
2900
    >>> string_to_delta('24h') == datetime.timedelta(1)
 
2901
    True
 
2902
    >>> string_to_delta('1w') == datetime.timedelta(7)
 
2903
    True
 
2904
    >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
 
2905
    True
2750
2906
    """
2751
2907
 
2752
2908
    try:
2854
3010
 
2855
3011
    options = parser.parse_args()
2856
3012
 
2857
 
    if options.check:
2858
 
        import doctest
2859
 
        fail_count, test_count = doctest.testmod()
2860
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2861
 
 
2862
3013
    # Default values for config file for server-global settings
 
3014
    if gnutls.has_rawpk:
 
3015
        priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
 
3016
                    ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
 
3017
    else:
 
3018
        priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
3019
                    ":+SIGN-DSA-SHA256")
2863
3020
    server_defaults = {"interface": "",
2864
3021
                       "address": "",
2865
3022
                       "port": "",
2866
3023
                       "debug": "False",
2867
 
                       "priority":
2868
 
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2869
 
                       ":+SIGN-DSA-SHA256",
 
3024
                       "priority": priority,
2870
3025
                       "servicename": "Mandos",
2871
3026
                       "use_dbus": "True",
2872
3027
                       "use_ipv6": "True",
2877
3032
                       "foreground": "False",
2878
3033
                       "zeroconf": "True",
2879
3034
                       }
 
3035
    del priority
2880
3036
 
2881
3037
    # Parse config file for server-global settings
2882
 
    server_config = configparser.SafeConfigParser(server_defaults)
 
3038
    server_config = configparser.ConfigParser(server_defaults)
2883
3039
    del server_defaults
2884
3040
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
2885
 
    # Convert the SafeConfigParser object to a dict
 
3041
    # Convert the ConfigParser object to a dict
2886
3042
    server_settings = server_config.defaults()
2887
3043
    # Use the appropriate methods on the non-string config options
2888
3044
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2960
3116
                                  server_settings["servicename"])))
2961
3117
 
2962
3118
    # Parse config file with clients
2963
 
    client_config = configparser.SafeConfigParser(Client
2964
 
                                                  .client_defaults)
 
3119
    client_config = configparser.ConfigParser(Client.client_defaults)
2965
3120
    client_config.read(os.path.join(server_settings["configdir"],
2966
3121
                                    "clients.conf"))
2967
3122
 
3038
3193
        # Close all input and output, do double fork, etc.
3039
3194
        daemon()
3040
3195
 
3041
 
    # multiprocessing will use threads, so before we use GLib we need
3042
 
    # to inform GLib that threads will be used.
3043
 
    GLib.threads_init()
 
3196
    if gi.version_info < (3, 10, 2):
 
3197
        # multiprocessing will use threads, so before we use GLib we
 
3198
        # need to inform GLib that threads will be used.
 
3199
        GLib.threads_init()
3044
3200
 
3045
3201
    global main_loop
3046
3202
    # From the Avahi example code
3122
3278
                             if isinstance(s, bytes)
3123
3279
                             else s) for s in
3124
3280
                            value["client_structure"]]
3125
 
                        # .name & .host
3126
 
                        for k in ("name", "host"):
 
3281
                        # .name, .host, and .checker_command
 
3282
                        for k in ("name", "host", "checker_command"):
3127
3283
                            if isinstance(value[k], bytes):
3128
3284
                                value[k] = value[k].decode("utf-8")
 
3285
                        if "key_id" not in value:
 
3286
                            value["key_id"] = ""
 
3287
                        elif "fingerprint" not in value:
 
3288
                            value["fingerprint"] = ""
3129
3289
                    #  old_client_settings
3130
3290
                    # .keys()
3131
3291
                    old_client_settings = {
3135
3295
                        for key, value in
3136
3296
                        bytes_old_client_settings.items()}
3137
3297
                    del bytes_old_client_settings
3138
 
                    # .host
 
3298
                    # .host and .checker_command
3139
3299
                    for value in old_client_settings.values():
3140
 
                        if isinstance(value["host"], bytes):
3141
 
                            value["host"] = (value["host"]
3142
 
                                             .decode("utf-8"))
 
3300
                        for attribute in ("host", "checker_command"):
 
3301
                            if isinstance(value[attribute], bytes):
 
3302
                                value[attribute] = (value[attribute]
 
3303
                                                    .decode("utf-8"))
3143
3304
            os.remove(stored_state_path)
3144
3305
        except IOError as e:
3145
3306
            if e.errno == errno.ENOENT:
3268
3429
                pass
3269
3430
 
3270
3431
            @dbus.service.signal(_interface, signature="ss")
3271
 
            def ClientNotFound(self, fingerprint, address):
 
3432
            def ClientNotFound(self, key_id, address):
3272
3433
                "D-Bus signal"
3273
3434
                pass
3274
3435
 
3470
3631
                sys.exit(1)
3471
3632
            # End of Avahi example code
3472
3633
 
3473
 
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3474
 
                          lambda *args, **kwargs:
3475
 
                          (tcp_server.handle_request
3476
 
                           (*args[2:], **kwargs) or True))
 
3634
        GLib.io_add_watch(
 
3635
            GLib.IOChannel.unix_new(tcp_server.fileno()),
 
3636
            GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
3637
            lambda *args, **kwargs: (tcp_server.handle_request
 
3638
                                     (*args[2:], **kwargs) or True))
3477
3639
 
3478
3640
        logger.debug("Starting main loop")
3479
3641
        main_loop.run()
3489
3651
    # Must run before the D-Bus bus name gets deregistered
3490
3652
    cleanup()
3491
3653
 
 
3654
 
 
3655
def should_only_run_tests():
 
3656
    parser = argparse.ArgumentParser(add_help=False)
 
3657
    parser.add_argument("--check", action='store_true')
 
3658
    args, unknown_args = parser.parse_known_args()
 
3659
    run_tests = args.check
 
3660
    if run_tests:
 
3661
        # Remove --check argument from sys.argv
 
3662
        sys.argv[1:] = unknown_args
 
3663
    return run_tests
 
3664
 
 
3665
# Add all tests from doctest strings
 
3666
def load_tests(loader, tests, none):
 
3667
    import doctest
 
3668
    tests.addTests(doctest.DocTestSuite())
 
3669
    return tests
3492
3670
 
3493
3671
if __name__ == '__main__':
3494
 
    main()
 
3672
    try:
 
3673
        if should_only_run_tests():
 
3674
            # Call using ./mandos --check [--verbose]
 
3675
            unittest.main()
 
3676
        else:
 
3677
            main()
 
3678
    finally:
 
3679
        logging.shutdown()