/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: 2017-08-20 14:14:14 UTC
  • Revision ID: teddy@recompile.se-20170820141414-m034xuebg7ccaeui
Add some more restrictions to the systemd service file.

* mandos.service ([Service]/ProtectKernelTunables): New; set to "yes".
  ([Service]/ProtectControlGroups): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
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 -*-
 
1
#!/usr/bin/python
 
2
# -*- mode: python; 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-2019 Teddy Hogeborn
15
 
# Copyright © 2008-2019 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
 
14
# Copyright © 2008-2017 Teddy Hogeborn
 
15
# Copyright © 2008-2017 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
21
19
# the Free Software Foundation, either version 3 of the License, or
22
20
# (at your option) any later version.
23
21
#
24
 
#     Mandos is distributed in the hope that it will be useful, but
25
 
#     WITHOUT ANY WARRANTY; without even the implied warranty of
 
22
#     This program is distributed in the hope that it will be useful,
 
23
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
26
24
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27
25
#     GNU General Public License for more details.
28
26
#
29
27
# You should have received a copy of the GNU General Public License
30
 
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
28
# along with this program.  If not, see
 
29
# <http://www.gnu.org/licenses/>.
31
30
#
32
31
# Contact the authors at <mandos@recompile.se>.
33
32
#
77
76
import itertools
78
77
import collections
79
78
import codecs
80
 
import unittest
81
 
import random
82
 
import shlex
83
79
 
84
80
import dbus
85
81
import dbus.service
86
 
import gi
87
82
from gi.repository import GLib
88
83
from dbus.mainloop.glib import DBusGMainLoop
89
84
import ctypes
91
86
import xml.dom.minidom
92
87
import inspect
93
88
 
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
 
 
118
89
# Try to find the value of SO_BINDTODEVICE:
119
90
try:
120
91
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
140
111
            # No value found
141
112
            SO_BINDTODEVICE = None
142
113
 
143
 
if sys.version_info < (3, 2):
144
 
    configparser.Configparser = configparser.SafeConfigParser
 
114
if sys.version_info.major == 2:
 
115
    str = unicode
145
116
 
146
 
version = "1.8.9"
 
117
version = "1.7.15"
147
118
stored_state_file = "clients.pickle"
148
119
 
149
120
logger = logging.getLogger()
150
 
logging.captureWarnings(True)   # Show warnings via the logging system
151
121
syslogger = None
152
122
 
153
123
try:
208
178
    pass
209
179
 
210
180
 
211
 
class PGPEngine:
 
181
class PGPEngine(object):
212
182
    """A simple class for OpenPGP symmetric encryption & decryption"""
213
183
 
214
184
    def __init__(self):
218
188
            output = subprocess.check_output(["gpgconf"])
219
189
            for line in output.splitlines():
220
190
                name, text, path = line.split(b":")
221
 
                if name == b"gpg":
 
191
                if name == "gpg":
222
192
                    self.gpg = path
223
193
                    break
224
194
        except OSError as e:
229
199
                          '--force-mdc',
230
200
                          '--quiet']
231
201
        # Only GPG version 1 has the --no-use-agent option.
232
 
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
 
202
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
233
203
            self.gnupgargs.append("--no-use-agent")
234
204
 
235
205
    def __enter__(self):
304
274
 
305
275
 
306
276
# Pretend that we have an Avahi module
307
 
class avahi:
308
 
    """This isn't so much a class as it is a module-like namespace."""
 
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."""
309
280
    IF_UNSPEC = -1               # avahi-common/address.h
310
281
    PROTO_UNSPEC = -1            # avahi-common/address.h
311
282
    PROTO_INET = 0               # avahi-common/address.h
315
286
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
316
287
    DBUS_PATH_SERVER = "/"
317
288
 
318
 
    @staticmethod
319
 
    def string_array_to_txt_array(t):
 
289
    def string_array_to_txt_array(self, t):
320
290
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
321
291
                           for s in t), signature="ay")
322
292
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
327
297
    SERVER_RUNNING = 2           # avahi-common/defs.h
328
298
    SERVER_COLLISION = 3         # avahi-common/defs.h
329
299
    SERVER_FAILURE = 4           # avahi-common/defs.h
 
300
avahi = Avahi()
330
301
 
331
302
 
332
303
class AvahiError(Exception):
344
315
    pass
345
316
 
346
317
 
347
 
class AvahiService:
 
318
class AvahiService(object):
348
319
    """An Avahi (Zeroconf) service.
349
320
 
350
321
    Attributes:
524
495
class AvahiServiceToSyslog(AvahiService):
525
496
    def rename(self, *args, **kwargs):
526
497
        """Add the new name to the syslog messages"""
527
 
        ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
 
498
        ret = AvahiService.rename(self, *args, **kwargs)
528
499
        syslogger.setFormatter(logging.Formatter(
529
500
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
530
501
            .format(self.name)))
532
503
 
533
504
 
534
505
# Pretend that we have a GnuTLS module
535
 
class gnutls:
536
 
    """This isn't so much a class as it is a module-like namespace."""
 
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."""
537
509
 
538
510
    library = ctypes.util.find_library("gnutls")
539
511
    if library is None:
540
512
        library = ctypes.util.find_library("gnutls-deb0")
541
513
    _library = ctypes.cdll.LoadLibrary(library)
542
514
    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))
543
523
 
544
524
    # Unless otherwise indicated, the constants and types below are
545
525
    # all from the gnutls/gnutls.h C header file.
549
529
    E_INTERRUPTED = -52
550
530
    E_AGAIN = -28
551
531
    CRT_OPENPGP = 2
552
 
    CRT_RAWPK = 3
553
532
    CLIENT = 2
554
533
    SHUT_RDWR = 0
555
534
    CRD_CERTIFICATE = 1
556
535
    E_NO_CERTIFICATE_FOUND = -49
557
 
    X509_FMT_DER = 0
558
 
    NO_TICKETS = 1<<10
559
 
    ENABLE_RAWPK = 1<<18
560
 
    CTYPE_PEERS = 3
561
 
    KEYID_USE_SHA256 = 1        # gnutls/x509.h
562
536
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
563
537
 
564
538
    # Types
587
561
 
588
562
    # Exceptions
589
563
    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.
590
568
        def __init__(self, message=None, code=None, args=()):
591
569
            # Default usage is by a message string, but if a return
592
570
            # code is passed, convert it to a string with
593
571
            # gnutls.strerror()
594
572
            self.code = code
595
573
            if message is None and code is not None:
596
 
                message = gnutls.strerror(code)
597
 
            return super(gnutls.Error, self).__init__(
 
574
                message = GnuTLS.strerror(code)
 
575
            return super(GnuTLS.Error, self).__init__(
598
576
                message, *args)
599
577
 
600
578
    class CertificateSecurityError(Error):
601
579
        pass
602
580
 
603
581
    # Classes
604
 
    class Credentials:
 
582
    class Credentials(object):
605
583
        def __init__(self):
606
584
            self._c_object = gnutls.certificate_credentials_t()
607
585
            gnutls.certificate_allocate_credentials(
611
589
        def __del__(self):
612
590
            gnutls.certificate_free_credentials(self._c_object)
613
591
 
614
 
    class ClientSession:
 
592
    class ClientSession(object):
615
593
        def __init__(self, socket, credentials=None):
616
594
            self._c_object = gnutls.session_t()
617
 
            gnutls_flags = gnutls.CLIENT
618
 
            if gnutls.check_version(b"3.5.6"):
619
 
                gnutls_flags |= gnutls.NO_TICKETS
620
 
            if gnutls.has_rawpk:
621
 
                gnutls_flags |= gnutls.ENABLE_RAWPK
622
 
            gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
623
 
            del gnutls_flags
 
595
            gnutls.init(ctypes.byref(self._c_object), gnutls.CLIENT)
624
596
            gnutls.set_default_priority(self._c_object)
625
597
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
626
598
            gnutls.handshake_set_private_extensions(self._c_object,
758
730
    check_version.argtypes = [ctypes.c_char_p]
759
731
    check_version.restype = ctypes.c_char_p
760
732
 
761
 
    _need_version = b"3.3.0"
762
 
    if check_version(_need_version) is None:
763
 
        raise self.Error("Needs GnuTLS {} or later"
764
 
                         .format(_need_version))
765
 
 
766
 
    _tls_rawpk_version = b"3.6.6"
767
 
    has_rawpk = bool(check_version(_tls_rawpk_version))
768
 
 
769
 
    if has_rawpk:
770
 
        # Types
771
 
        class pubkey_st(ctypes.Structure):
772
 
            _fields = []
773
 
        pubkey_t = ctypes.POINTER(pubkey_st)
774
 
 
775
 
        x509_crt_fmt_t = ctypes.c_int
776
 
 
777
 
        # All the function declarations below are from gnutls/abstract.h
778
 
        pubkey_init = _library.gnutls_pubkey_init
779
 
        pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
780
 
        pubkey_init.restype = _error_code
781
 
 
782
 
        pubkey_import = _library.gnutls_pubkey_import
783
 
        pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
784
 
                                  x509_crt_fmt_t]
785
 
        pubkey_import.restype = _error_code
786
 
 
787
 
        pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
788
 
        pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
789
 
                                      ctypes.POINTER(ctypes.c_ubyte),
790
 
                                      ctypes.POINTER(ctypes.c_size_t)]
791
 
        pubkey_get_key_id.restype = _error_code
792
 
 
793
 
        pubkey_deinit = _library.gnutls_pubkey_deinit
794
 
        pubkey_deinit.argtypes = [pubkey_t]
795
 
        pubkey_deinit.restype = None
796
 
    else:
797
 
        # All the function declarations below are from gnutls/openpgp.h
798
 
 
799
 
        openpgp_crt_init = _library.gnutls_openpgp_crt_init
800
 
        openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
801
 
        openpgp_crt_init.restype = _error_code
802
 
 
803
 
        openpgp_crt_import = _library.gnutls_openpgp_crt_import
804
 
        openpgp_crt_import.argtypes = [openpgp_crt_t,
805
 
                                       ctypes.POINTER(datum_t),
806
 
                                       openpgp_crt_fmt_t]
807
 
        openpgp_crt_import.restype = _error_code
808
 
 
809
 
        openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
810
 
        openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
811
 
                                            ctypes.POINTER(ctypes.c_uint)]
812
 
        openpgp_crt_verify_self.restype = _error_code
813
 
 
814
 
        openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
815
 
        openpgp_crt_deinit.argtypes = [openpgp_crt_t]
816
 
        openpgp_crt_deinit.restype = None
817
 
 
818
 
        openpgp_crt_get_fingerprint = (
819
 
            _library.gnutls_openpgp_crt_get_fingerprint)
820
 
        openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
821
 
                                                ctypes.c_void_p,
822
 
                                                ctypes.POINTER(
823
 
                                                    ctypes.c_size_t)]
824
 
        openpgp_crt_get_fingerprint.restype = _error_code
825
 
 
826
 
    if check_version(b"3.6.4"):
827
 
        certificate_type_get2 = _library.gnutls_certificate_type_get2
828
 
        certificate_type_get2.argtypes = [session_t, ctypes.c_int]
829
 
        certificate_type_get2.restype = _error_code
 
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
830
761
 
831
762
    # Remove non-public functions
832
763
    del _error_code, _retry_on_error
 
764
# Create the global "gnutls" object, simulating a module
 
765
gnutls = GnuTLS()
833
766
 
834
767
 
835
768
def call_pipe(connection,       # : multiprocessing.Connection
843
776
    connection.close()
844
777
 
845
778
 
846
 
class Client:
 
779
class Client(object):
847
780
    """A representation of a client host served by this server.
848
781
 
849
782
    Attributes:
850
783
    approved:   bool(); 'None' if not yet approved/disapproved
851
784
    approval_delay: datetime.timedelta(); Time to wait for approval
852
785
    approval_duration: datetime.timedelta(); Duration of one approval
853
 
    checker: multiprocessing.Process(); a running checker process used
854
 
             to see if the client lives. 'None' if no process is
855
 
             running.
 
786
    checker:    subprocess.Popen(); a running checker process used
 
787
                                    to see if the client lives.
 
788
                                    'None' if no process is running.
856
789
    checker_callback_tag: a GLib event source tag, or None
857
790
    checker_command: string; External command which is run to check
858
791
                     if client lives.  %() expansions are done at
866
799
    disable_initiator_tag: a GLib event source tag, or None
867
800
    enabled:    bool()
868
801
    fingerprint: string (40 or 32 hexadecimal digits); used to
869
 
                 uniquely identify an OpenPGP client
870
 
    key_id: string (64 hexadecimal digits); used to uniquely identify
871
 
            a client using raw public keys
 
802
                 uniquely identify the client
872
803
    host:       string; available for use by the checker command
873
804
    interval:   datetime.timedelta(); How often to start a new checker
874
805
    last_approval_request: datetime.datetime(); (UTC) or None
892
823
    """
893
824
 
894
825
    runtime_expansions = ("approval_delay", "approval_duration",
895
 
                          "created", "enabled", "expires", "key_id",
 
826
                          "created", "enabled", "expires",
896
827
                          "fingerprint", "host", "interval",
897
828
                          "last_approval_request", "last_checked_ok",
898
829
                          "last_enabled", "name", "timeout")
928
859
            client["enabled"] = config.getboolean(client_name,
929
860
                                                  "enabled")
930
861
 
931
 
            # Uppercase and remove spaces from key_id and fingerprint
932
 
            # for later comparison purposes with return value from the
933
 
            # key_id() and fingerprint() functions
934
 
            client["key_id"] = (section.get("key_id", "").upper()
935
 
                                .replace(" ", ""))
 
862
            # Uppercase and remove spaces from fingerprint for later
 
863
            # comparison purposes with return value from the
 
864
            # fingerprint() function
936
865
            client["fingerprint"] = (section["fingerprint"].upper()
937
866
                                     .replace(" ", ""))
938
867
            if "secret" in section:
982
911
            self.expires = None
983
912
 
984
913
        logger.debug("Creating client %r", self.name)
985
 
        logger.debug("  Key ID: %s", self.key_id)
986
914
        logger.debug("  Fingerprint: %s", self.fingerprint)
987
915
        self.created = settings.get("created",
988
916
                                    datetime.datetime.utcnow())
1052
980
        if self.checker_initiator_tag is not None:
1053
981
            GLib.source_remove(self.checker_initiator_tag)
1054
982
        self.checker_initiator_tag = GLib.timeout_add(
1055
 
            random.randrange(int(self.interval.total_seconds() * 1000
1056
 
                                 + 1)),
 
983
            int(self.interval.total_seconds() * 1000),
1057
984
            self.start_checker)
1058
985
        # Schedule a disable() when 'timeout' has passed
1059
986
        if self.disable_initiator_tag is not None:
1066
993
    def checker_callback(self, source, condition, connection,
1067
994
                         command):
1068
995
        """The checker has completed, so take appropriate actions."""
 
996
        self.checker_callback_tag = None
 
997
        self.checker = None
1069
998
        # Read return code from connection (see call_pipe)
1070
999
        returncode = connection.recv()
1071
1000
        connection.close()
1072
 
        if self.checker is not None:
1073
 
            self.checker.join()
1074
 
        self.checker_callback_tag = None
1075
 
        self.checker = None
1076
1001
 
1077
1002
        if returncode >= 0:
1078
1003
            self.last_checker_status = returncode
1134
1059
        if self.checker is None:
1135
1060
            # Escape attributes for the shell
1136
1061
            escaped_attrs = {
1137
 
                attr: shlex.quote(str(getattr(self, attr)))
 
1062
                attr: re.escape(str(getattr(self, attr)))
1138
1063
                for attr in self.runtime_expansions}
1139
1064
            try:
1140
1065
                command = self.checker_command % escaped_attrs
1167
1092
                kwargs=popen_args)
1168
1093
            self.checker.start()
1169
1094
            self.checker_callback_tag = GLib.io_add_watch(
1170
 
                GLib.IOChannel.unix_new(pipe[0].fileno()),
1171
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
 
1095
                pipe[0].fileno(), GLib.IO_IN,
1172
1096
                self.checker_callback, pipe[0], command)
1173
1097
        # Re-run this periodically if run by GLib.timeout_add
1174
1098
        return True
1429
1353
                raise ValueError("Byte arrays not supported for non-"
1430
1354
                                 "'ay' signature {!r}"
1431
1355
                                 .format(prop._dbus_signature))
1432
 
            value = dbus.ByteArray(bytes(value))
 
1356
            value = dbus.ByteArray(b''.join(chr(byte)
 
1357
                                            for byte in value))
1433
1358
        prop(value)
1434
1359
 
1435
1360
    @dbus.service.method(dbus.PROPERTIES_IFACE,
2073
1998
    def Name_dbus_property(self):
2074
1999
        return dbus.String(self.name)
2075
2000
 
2076
 
    # KeyID - property
2077
 
    @dbus_annotations(
2078
 
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2079
 
    @dbus_service_property(_interface, signature="s", access="read")
2080
 
    def KeyID_dbus_property(self):
2081
 
        return dbus.String(self.key_id)
2082
 
 
2083
2001
    # Fingerprint - property
2084
2002
    @dbus_annotations(
2085
2003
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2240
2158
    del _interface
2241
2159
 
2242
2160
 
2243
 
class ProxyClient:
2244
 
    def __init__(self, child_pipe, key_id, fpr, address):
 
2161
class ProxyClient(object):
 
2162
    def __init__(self, child_pipe, fpr, address):
2245
2163
        self._pipe = child_pipe
2246
 
        self._pipe.send(('init', key_id, fpr, address))
 
2164
        self._pipe.send(('init', fpr, address))
2247
2165
        if not self._pipe.recv():
2248
 
            raise KeyError(key_id or fpr)
 
2166
            raise KeyError(fpr)
2249
2167
 
2250
2168
    def __getattribute__(self, name):
2251
2169
        if name == '_pipe':
2318
2236
 
2319
2237
            approval_required = False
2320
2238
            try:
2321
 
                if gnutls.has_rawpk:
2322
 
                    fpr = b""
2323
 
                    try:
2324
 
                        key_id = self.key_id(
2325
 
                            self.peer_certificate(session))
2326
 
                    except (TypeError, gnutls.Error) as error:
2327
 
                        logger.warning("Bad certificate: %s", error)
2328
 
                        return
2329
 
                    logger.debug("Key ID: %s", key_id)
2330
 
 
2331
 
                else:
2332
 
                    key_id = b""
2333
 
                    try:
2334
 
                        fpr = self.fingerprint(
2335
 
                            self.peer_certificate(session))
2336
 
                    except (TypeError, gnutls.Error) as error:
2337
 
                        logger.warning("Bad certificate: %s", error)
2338
 
                        return
2339
 
                    logger.debug("Fingerprint: %s", fpr)
2340
 
 
2341
 
                try:
2342
 
                    client = ProxyClient(child_pipe, key_id, fpr,
 
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,
2343
2249
                                         self.client_address)
2344
2250
                except KeyError:
2345
2251
                    return
2422
2328
 
2423
2329
    @staticmethod
2424
2330
    def peer_certificate(session):
2425
 
        "Return the peer's certificate as a bytestring"
2426
 
        try:
2427
 
            cert_type = gnutls.certificate_type_get2(session._c_object,
2428
 
                                                     gnutls.CTYPE_PEERS)
2429
 
        except AttributeError:
2430
 
            cert_type = gnutls.certificate_type_get(session._c_object)
2431
 
        if gnutls.has_rawpk:
2432
 
            valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
2433
 
        else:
2434
 
            valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2435
 
        # If not a valid certificate type...
2436
 
        if cert_type not in valid_cert_types:
2437
 
            logger.info("Cert type %r not in %r", cert_type,
2438
 
                        valid_cert_types)
 
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):
2439
2335
            # ...return invalid data
2440
2336
            return b""
2441
2337
        list_size = ctypes.c_uint(1)
2449
2345
        return ctypes.string_at(cert.data, cert.size)
2450
2346
 
2451
2347
    @staticmethod
2452
 
    def key_id(certificate):
2453
 
        "Convert a certificate bytestring to a hexdigit key ID"
2454
 
        # New GnuTLS "datum" with the public key
2455
 
        datum = gnutls.datum_t(
2456
 
            ctypes.cast(ctypes.c_char_p(certificate),
2457
 
                        ctypes.POINTER(ctypes.c_ubyte)),
2458
 
            ctypes.c_uint(len(certificate)))
2459
 
        # XXX all these need to be created in the gnutls "module"
2460
 
        # New empty GnuTLS certificate
2461
 
        pubkey = gnutls.pubkey_t()
2462
 
        gnutls.pubkey_init(ctypes.byref(pubkey))
2463
 
        # Import the raw public key into the certificate
2464
 
        gnutls.pubkey_import(pubkey,
2465
 
                             ctypes.byref(datum),
2466
 
                             gnutls.X509_FMT_DER)
2467
 
        # New buffer for the key ID
2468
 
        buf = ctypes.create_string_buffer(32)
2469
 
        buf_len = ctypes.c_size_t(len(buf))
2470
 
        # Get the key ID from the raw public key into the buffer
2471
 
        gnutls.pubkey_get_key_id(pubkey,
2472
 
                                 gnutls.KEYID_USE_SHA256,
2473
 
                                 ctypes.cast(ctypes.byref(buf),
2474
 
                                             ctypes.POINTER(ctypes.c_ubyte)),
2475
 
                                 ctypes.byref(buf_len))
2476
 
        # Deinit the certificate
2477
 
        gnutls.pubkey_deinit(pubkey)
2478
 
 
2479
 
        # Convert the buffer to a Python bytestring
2480
 
        key_id = ctypes.string_at(buf, buf_len.value)
2481
 
        # Convert the bytestring to hexadecimal notation
2482
 
        hex_key_id = binascii.hexlify(key_id).upper()
2483
 
        return hex_key_id
2484
 
 
2485
 
    @staticmethod
2486
2348
    def fingerprint(openpgp):
2487
2349
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2488
2350
        # New GnuTLS "datum" with the OpenPGP public key
2502
2364
                                       ctypes.byref(crtverify))
2503
2365
        if crtverify.value != 0:
2504
2366
            gnutls.openpgp_crt_deinit(crt)
2505
 
            raise gnutls.CertificateSecurityError(code
2506
 
                                                  =crtverify.value)
 
2367
            raise gnutls.CertificateSecurityError("Verify failed")
2507
2368
        # New buffer for the fingerprint
2508
2369
        buf = ctypes.create_string_buffer(20)
2509
2370
        buf_len = ctypes.c_size_t()
2519
2380
        return hex_fpr
2520
2381
 
2521
2382
 
2522
 
class MultiprocessingMixIn:
 
2383
class MultiprocessingMixIn(object):
2523
2384
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2524
2385
 
2525
2386
    def sub_process_main(self, request, address):
2537
2398
        return proc
2538
2399
 
2539
2400
 
2540
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
 
2401
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2541
2402
    """ adds a pipe to the MixIn """
2542
2403
 
2543
2404
    def process_request(self, request, client_address):
2558
2419
 
2559
2420
 
2560
2421
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2561
 
                     socketserver.TCPServer):
 
2422
                     socketserver.TCPServer, object):
2562
2423
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2563
2424
 
2564
2425
    Attributes:
2637
2498
                    raise
2638
2499
        # Only bind(2) the socket if we really need to.
2639
2500
        if self.server_address[0] or self.server_address[1]:
2640
 
            if self.server_address[1]:
2641
 
                self.allow_reuse_address = True
2642
2501
            if not self.server_address[0]:
2643
2502
                if self.address_family == socket.AF_INET6:
2644
2503
                    any_address = "::"  # in6addr_any
2697
2556
    def add_pipe(self, parent_pipe, proc):
2698
2557
        # Call "handle_ipc" for both data and EOF events
2699
2558
        GLib.io_add_watch(
2700
 
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
2701
 
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
 
2559
            parent_pipe.fileno(),
 
2560
            GLib.IO_IN | GLib.IO_HUP,
2702
2561
            functools.partial(self.handle_ipc,
2703
2562
                              parent_pipe=parent_pipe,
2704
2563
                              proc=proc))
2718
2577
        command = request[0]
2719
2578
 
2720
2579
        if command == 'init':
2721
 
            key_id = request[1].decode("ascii")
2722
 
            fpr = request[2].decode("ascii")
2723
 
            address = request[3]
 
2580
            fpr = request[1]
 
2581
            address = request[2]
2724
2582
 
2725
2583
            for c in self.clients.values():
2726
 
                if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
2727
 
                    continue
2728
 
                if key_id and c.key_id == key_id:
2729
 
                    client = c
2730
 
                    break
2731
 
                if fpr and c.fingerprint == fpr:
 
2584
                if c.fingerprint == fpr:
2732
2585
                    client = c
2733
2586
                    break
2734
2587
            else:
2735
 
                logger.info("Client not found for key ID: %s, address"
2736
 
                            ": %s", key_id or fpr, address)
 
2588
                logger.info("Client not found for fingerprint: %s, ad"
 
2589
                            "dress: %s", fpr, address)
2737
2590
                if self.use_dbus:
2738
2591
                    # Emit D-Bus signal
2739
 
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
 
2592
                    mandos_dbus_service.ClientNotFound(fpr,
2740
2593
                                                       address[0])
2741
2594
                parent_pipe.send(False)
2742
2595
                return False
2743
2596
 
2744
2597
            GLib.io_add_watch(
2745
 
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
2746
 
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
 
2598
                parent_pipe.fileno(),
 
2599
                GLib.IO_IN | GLib.IO_HUP,
2747
2600
                functools.partial(self.handle_ipc,
2748
2601
                                  parent_pipe=parent_pipe,
2749
2602
                                  proc=proc,
2764
2617
        if command == 'getattr':
2765
2618
            attrname = request[1]
2766
2619
            if isinstance(client_object.__getattribute__(attrname),
2767
 
                          collections.abc.Callable):
 
2620
                          collections.Callable):
2768
2621
                parent_pipe.send(('function', ))
2769
2622
            else:
2770
2623
                parent_pipe.send((
2781
2634
def rfc3339_duration_to_delta(duration):
2782
2635
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2783
2636
 
2784
 
    >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
2785
 
    True
2786
 
    >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
2787
 
    True
2788
 
    >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600)
2789
 
    True
2790
 
    >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
2791
 
    True
2792
 
    >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
2793
 
    True
2794
 
    >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
2795
 
    True
2796
 
    >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
2797
 
    True
 
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)
2798
2651
    """
2799
2652
 
2800
2653
    # Parsing an RFC 3339 duration with regular expressions is not
2880
2733
def string_to_delta(interval):
2881
2734
    """Parse a string and return a datetime.timedelta
2882
2735
 
2883
 
    >>> string_to_delta('7d') == datetime.timedelta(7)
2884
 
    True
2885
 
    >>> string_to_delta('60s') == datetime.timedelta(0, 60)
2886
 
    True
2887
 
    >>> string_to_delta('60m') == datetime.timedelta(0, 3600)
2888
 
    True
2889
 
    >>> string_to_delta('24h') == datetime.timedelta(1)
2890
 
    True
2891
 
    >>> string_to_delta('1w') == datetime.timedelta(7)
2892
 
    True
2893
 
    >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
2894
 
    True
 
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)
2895
2748
    """
2896
2749
 
2897
2750
    try:
2999
2852
 
3000
2853
    options = parser.parse_args()
3001
2854
 
 
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
 
3002
2860
    # Default values for config file for server-global settings
3003
 
    if gnutls.has_rawpk:
3004
 
        priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
3005
 
                    ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
3006
 
    else:
3007
 
        priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
3008
 
                    ":+SIGN-DSA-SHA256")
3009
2861
    server_defaults = {"interface": "",
3010
2862
                       "address": "",
3011
2863
                       "port": "",
3012
2864
                       "debug": "False",
3013
 
                       "priority": priority,
 
2865
                       "priority":
 
2866
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
 
2867
                       ":+SIGN-DSA-SHA256",
3014
2868
                       "servicename": "Mandos",
3015
2869
                       "use_dbus": "True",
3016
2870
                       "use_ipv6": "True",
3021
2875
                       "foreground": "False",
3022
2876
                       "zeroconf": "True",
3023
2877
                       }
3024
 
    del priority
3025
2878
 
3026
2879
    # Parse config file for server-global settings
3027
 
    server_config = configparser.ConfigParser(server_defaults)
 
2880
    server_config = configparser.SafeConfigParser(server_defaults)
3028
2881
    del server_defaults
3029
2882
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
3030
 
    # Convert the ConfigParser object to a dict
 
2883
    # Convert the SafeConfigParser object to a dict
3031
2884
    server_settings = server_config.defaults()
3032
2885
    # Use the appropriate methods on the non-string config options
3033
2886
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
3105
2958
                                  server_settings["servicename"])))
3106
2959
 
3107
2960
    # Parse config file with clients
3108
 
    client_config = configparser.ConfigParser(Client.client_defaults)
 
2961
    client_config = configparser.SafeConfigParser(Client
 
2962
                                                  .client_defaults)
3109
2963
    client_config.read(os.path.join(server_settings["configdir"],
3110
2964
                                    "clients.conf"))
3111
2965
 
3182
3036
        # Close all input and output, do double fork, etc.
3183
3037
        daemon()
3184
3038
 
3185
 
    if gi.version_info < (3, 10, 2):
3186
 
        # multiprocessing will use threads, so before we use GLib we
3187
 
        # need to inform GLib that threads will be used.
3188
 
        GLib.threads_init()
 
3039
    # multiprocessing will use threads, so before we use GLib we need
 
3040
    # to inform GLib that threads will be used.
 
3041
    GLib.threads_init()
3189
3042
 
3190
3043
    global main_loop
3191
3044
    # From the Avahi example code
3267
3120
                             if isinstance(s, bytes)
3268
3121
                             else s) for s in
3269
3122
                            value["client_structure"]]
3270
 
                        # .name, .host, and .checker_command
3271
 
                        for k in ("name", "host", "checker_command"):
 
3123
                        # .name & .host
 
3124
                        for k in ("name", "host"):
3272
3125
                            if isinstance(value[k], bytes):
3273
3126
                                value[k] = value[k].decode("utf-8")
3274
 
                        if "key_id" not in value:
3275
 
                            value["key_id"] = ""
3276
 
                        elif "fingerprint" not in value:
3277
 
                            value["fingerprint"] = ""
3278
3127
                    #  old_client_settings
3279
3128
                    # .keys()
3280
3129
                    old_client_settings = {
3284
3133
                        for key, value in
3285
3134
                        bytes_old_client_settings.items()}
3286
3135
                    del bytes_old_client_settings
3287
 
                    # .host and .checker_command
 
3136
                    # .host
3288
3137
                    for value in old_client_settings.values():
3289
 
                        for attribute in ("host", "checker_command"):
3290
 
                            if isinstance(value[attribute], bytes):
3291
 
                                value[attribute] = (value[attribute]
3292
 
                                                    .decode("utf-8"))
 
3138
                        if isinstance(value["host"], bytes):
 
3139
                            value["host"] = (value["host"]
 
3140
                                             .decode("utf-8"))
3293
3141
            os.remove(stored_state_path)
3294
3142
        except IOError as e:
3295
3143
            if e.errno == errno.ENOENT:
3418
3266
                pass
3419
3267
 
3420
3268
            @dbus.service.signal(_interface, signature="ss")
3421
 
            def ClientNotFound(self, key_id, address):
 
3269
            def ClientNotFound(self, fingerprint, address):
3422
3270
                "D-Bus signal"
3423
3271
                pass
3424
3272
 
3620
3468
                sys.exit(1)
3621
3469
            # End of Avahi example code
3622
3470
 
3623
 
        GLib.io_add_watch(
3624
 
            GLib.IOChannel.unix_new(tcp_server.fileno()),
3625
 
            GLib.PRIORITY_DEFAULT, GLib.IO_IN,
3626
 
            lambda *args, **kwargs: (tcp_server.handle_request
3627
 
                                     (*args[2:], **kwargs) or True))
 
3471
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
 
3472
                          lambda *args, **kwargs:
 
3473
                          (tcp_server.handle_request
 
3474
                           (*args[2:], **kwargs) or True))
3628
3475
 
3629
3476
        logger.debug("Starting main loop")
3630
3477
        main_loop.run()
3640
3487
    # Must run before the D-Bus bus name gets deregistered
3641
3488
    cleanup()
3642
3489
 
3643
 
 
3644
 
def should_only_run_tests():
3645
 
    parser = argparse.ArgumentParser(add_help=False)
3646
 
    parser.add_argument("--check", action='store_true')
3647
 
    args, unknown_args = parser.parse_known_args()
3648
 
    run_tests = args.check
3649
 
    if run_tests:
3650
 
        # Remove --check argument from sys.argv
3651
 
        sys.argv[1:] = unknown_args
3652
 
    return run_tests
3653
 
 
3654
 
# Add all tests from doctest strings
3655
 
def load_tests(loader, tests, none):
3656
 
    import doctest
3657
 
    tests.addTests(doctest.DocTestSuite())
3658
 
    return tests
3659
3490
 
3660
3491
if __name__ == '__main__':
3661
 
    try:
3662
 
        if should_only_run_tests():
3663
 
            # Call using ./mandos --check [--verbose]
3664
 
            unittest.main()
3665
 
        else:
3666
 
            main()
3667
 
    finally:
3668
 
        logging.shutdown()
 
3492
    main()