/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
16
 
#
17
 
# This program is free software: you can redistribute it and/or modify
18
 
# it under the terms of the GNU General Public License as published by
 
14
# Copyright © 2008-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
19
21
# the Free Software Foundation, either version 3 of the License, or
20
22
# (at your option) any later version.
21
23
#
22
 
#     This program is distributed in the hope that it will be useful,
23
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
24
#     Mandos is distributed in the hope that it will be useful, but
 
25
#     WITHOUT ANY WARRANTY; without even the implied warranty of
24
26
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
27
#     GNU General Public License for more details.
26
28
#
27
29
# You should have received a copy of the GNU General Public License
28
 
# along with this program.  If not, see
29
 
# <http://www.gnu.org/licenses/>.
 
30
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
30
31
#
31
32
# Contact the authors at <mandos@recompile.se>.
32
33
#
76
77
import itertools
77
78
import collections
78
79
import codecs
 
80
import unittest
 
81
import random
79
82
 
80
83
import dbus
81
84
import dbus.service
 
85
import gi
82
86
from gi.repository import GLib
83
87
from dbus.mainloop.glib import DBusGMainLoop
84
88
import ctypes
86
90
import xml.dom.minidom
87
91
import inspect
88
92
 
 
93
if sys.version_info.major == 2:
 
94
    __metaclass__ = type
 
95
    str = unicode
 
96
 
 
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
 
89
111
# Try to find the value of SO_BINDTODEVICE:
90
112
try:
91
113
    # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
111
133
            # No value found
112
134
            SO_BINDTODEVICE = None
113
135
 
114
 
if sys.version_info.major == 2:
115
 
    str = unicode
 
136
if sys.version_info < (3, 2):
 
137
    configparser.Configparser = configparser.SafeConfigParser
116
138
 
117
 
version = "1.7.15"
 
139
version = "1.8.9"
118
140
stored_state_file = "clients.pickle"
119
141
 
120
142
logger = logging.getLogger()
 
143
logging.captureWarnings(True)   # Show warnings via the logging system
121
144
syslogger = None
122
145
 
123
146
try:
178
201
    pass
179
202
 
180
203
 
181
 
class PGPEngine(object):
 
204
class PGPEngine:
182
205
    """A simple class for OpenPGP symmetric encryption & decryption"""
183
206
 
184
207
    def __init__(self):
188
211
            output = subprocess.check_output(["gpgconf"])
189
212
            for line in output.splitlines():
190
213
                name, text, path = line.split(b":")
191
 
                if name == "gpg":
 
214
                if name == b"gpg":
192
215
                    self.gpg = path
193
216
                    break
194
217
        except OSError as e:
199
222
                          '--force-mdc',
200
223
                          '--quiet']
201
224
        # Only GPG version 1 has the --no-use-agent option.
202
 
        if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
 
225
        if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
203
226
            self.gnupgargs.append("--no-use-agent")
204
227
 
205
228
    def __enter__(self):
274
297
 
275
298
 
276
299
# Pretend that we have an Avahi module
277
 
class Avahi(object):
278
 
    """This isn't so much a class as it is a module-like namespace.
279
 
    It is instantiated once, and simulates having an Avahi module."""
 
300
class avahi:
 
301
    """This isn't so much a class as it is a module-like namespace."""
280
302
    IF_UNSPEC = -1               # avahi-common/address.h
281
303
    PROTO_UNSPEC = -1            # avahi-common/address.h
282
304
    PROTO_INET = 0               # avahi-common/address.h
286
308
    DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
287
309
    DBUS_PATH_SERVER = "/"
288
310
 
289
 
    def string_array_to_txt_array(self, t):
 
311
    @staticmethod
 
312
    def string_array_to_txt_array(t):
290
313
        return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
291
314
                           for s in t), signature="ay")
292
315
    ENTRY_GROUP_ESTABLISHED = 2  # avahi-common/defs.h
297
320
    SERVER_RUNNING = 2           # avahi-common/defs.h
298
321
    SERVER_COLLISION = 3         # avahi-common/defs.h
299
322
    SERVER_FAILURE = 4           # avahi-common/defs.h
300
 
avahi = Avahi()
301
323
 
302
324
 
303
325
class AvahiError(Exception):
315
337
    pass
316
338
 
317
339
 
318
 
class AvahiService(object):
 
340
class AvahiService:
319
341
    """An Avahi (Zeroconf) service.
320
342
 
321
343
    Attributes:
495
517
class AvahiServiceToSyslog(AvahiService):
496
518
    def rename(self, *args, **kwargs):
497
519
        """Add the new name to the syslog messages"""
498
 
        ret = AvahiService.rename(self, *args, **kwargs)
 
520
        ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
499
521
        syslogger.setFormatter(logging.Formatter(
500
522
            'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
501
523
            .format(self.name)))
503
525
 
504
526
 
505
527
# Pretend that we have a GnuTLS module
506
 
class GnuTLS(object):
507
 
    """This isn't so much a class as it is a module-like namespace.
508
 
    It is instantiated once, and simulates having a GnuTLS module."""
 
528
class gnutls:
 
529
    """This isn't so much a class as it is a module-like namespace."""
509
530
 
510
531
    library = ctypes.util.find_library("gnutls")
511
532
    if library is None:
512
533
        library = ctypes.util.find_library("gnutls-deb0")
513
534
    _library = ctypes.cdll.LoadLibrary(library)
514
535
    del library
515
 
    _need_version = b"3.3.0"
516
 
 
517
 
    def __init__(self):
518
 
        # Need to use "self" here, since this method is called before
519
 
        # the assignment to the "gnutls" global variable happens.
520
 
        if self.check_version(self._need_version) is None:
521
 
            raise self.Error("Needs GnuTLS {} or later"
522
 
                             .format(self._need_version))
523
536
 
524
537
    # Unless otherwise indicated, the constants and types below are
525
538
    # all from the gnutls/gnutls.h C header file.
529
542
    E_INTERRUPTED = -52
530
543
    E_AGAIN = -28
531
544
    CRT_OPENPGP = 2
 
545
    CRT_RAWPK = 3
532
546
    CLIENT = 2
533
547
    SHUT_RDWR = 0
534
548
    CRD_CERTIFICATE = 1
535
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
536
555
    OPENPGP_FMT_RAW = 0         # gnutls/openpgp.h
537
556
 
538
557
    # Types
561
580
 
562
581
    # Exceptions
563
582
    class Error(Exception):
564
 
        # We need to use the class name "GnuTLS" here, since this
565
 
        # exception might be raised from within GnuTLS.__init__,
566
 
        # which is called before the assignment to the "gnutls"
567
 
        # global variable has happened.
568
583
        def __init__(self, message=None, code=None, args=()):
569
584
            # Default usage is by a message string, but if a return
570
585
            # code is passed, convert it to a string with
571
586
            # gnutls.strerror()
572
587
            self.code = code
573
588
            if message is None and code is not None:
574
 
                message = GnuTLS.strerror(code)
575
 
            return super(GnuTLS.Error, self).__init__(
 
589
                message = gnutls.strerror(code)
 
590
            return super(gnutls.Error, self).__init__(
576
591
                message, *args)
577
592
 
578
593
    class CertificateSecurityError(Error):
579
594
        pass
580
595
 
581
596
    # Classes
582
 
    class Credentials(object):
 
597
    class Credentials:
583
598
        def __init__(self):
584
599
            self._c_object = gnutls.certificate_credentials_t()
585
600
            gnutls.certificate_allocate_credentials(
589
604
        def __del__(self):
590
605
            gnutls.certificate_free_credentials(self._c_object)
591
606
 
592
 
    class ClientSession(object):
 
607
    class ClientSession:
593
608
        def __init__(self, socket, credentials=None):
594
609
            self._c_object = gnutls.session_t()
595
 
            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
596
617
            gnutls.set_default_priority(self._c_object)
597
618
            gnutls.transport_set_ptr(self._c_object, socket.fileno())
598
619
            gnutls.handshake_set_private_extensions(self._c_object,
730
751
    check_version.argtypes = [ctypes.c_char_p]
731
752
    check_version.restype = ctypes.c_char_p
732
753
 
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
 
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
761
823
 
762
824
    # Remove non-public functions
763
825
    del _error_code, _retry_on_error
764
 
# Create the global "gnutls" object, simulating a module
765
 
gnutls = GnuTLS()
766
826
 
767
827
 
768
828
def call_pipe(connection,       # : multiprocessing.Connection
776
836
    connection.close()
777
837
 
778
838
 
779
 
class Client(object):
 
839
class Client:
780
840
    """A representation of a client host served by this server.
781
841
 
782
842
    Attributes:
783
843
    approved:   bool(); 'None' if not yet approved/disapproved
784
844
    approval_delay: datetime.timedelta(); Time to wait for approval
785
845
    approval_duration: datetime.timedelta(); Duration of one approval
786
 
    checker:    subprocess.Popen(); a running checker process used
787
 
                                    to see if the client lives.
788
 
                                    'None' if no process is running.
 
846
    checker: multiprocessing.Process(); a running checker process used
 
847
             to see if the client lives. 'None' if no process is
 
848
             running.
789
849
    checker_callback_tag: a GLib event source tag, or None
790
850
    checker_command: string; External command which is run to check
791
851
                     if client lives.  %() expansions are done at
799
859
    disable_initiator_tag: a GLib event source tag, or None
800
860
    enabled:    bool()
801
861
    fingerprint: string (40 or 32 hexadecimal digits); used to
802
 
                 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
803
865
    host:       string; available for use by the checker command
804
866
    interval:   datetime.timedelta(); How often to start a new checker
805
867
    last_approval_request: datetime.datetime(); (UTC) or None
823
885
    """
824
886
 
825
887
    runtime_expansions = ("approval_delay", "approval_duration",
826
 
                          "created", "enabled", "expires",
 
888
                          "created", "enabled", "expires", "key_id",
827
889
                          "fingerprint", "host", "interval",
828
890
                          "last_approval_request", "last_checked_ok",
829
891
                          "last_enabled", "name", "timeout")
859
921
            client["enabled"] = config.getboolean(client_name,
860
922
                                                  "enabled")
861
923
 
862
 
            # Uppercase and remove spaces from fingerprint for later
863
 
            # comparison purposes with return value from the
864
 
            # 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(" ", ""))
865
929
            client["fingerprint"] = (section["fingerprint"].upper()
866
930
                                     .replace(" ", ""))
867
931
            if "secret" in section:
911
975
            self.expires = None
912
976
 
913
977
        logger.debug("Creating client %r", self.name)
 
978
        logger.debug("  Key ID: %s", self.key_id)
914
979
        logger.debug("  Fingerprint: %s", self.fingerprint)
915
980
        self.created = settings.get("created",
916
981
                                    datetime.datetime.utcnow())
980
1045
        if self.checker_initiator_tag is not None:
981
1046
            GLib.source_remove(self.checker_initiator_tag)
982
1047
        self.checker_initiator_tag = GLib.timeout_add(
983
 
            int(self.interval.total_seconds() * 1000),
 
1048
            random.randrange(int(self.interval.total_seconds() * 1000
 
1049
                                 + 1)),
984
1050
            self.start_checker)
985
1051
        # Schedule a disable() when 'timeout' has passed
986
1052
        if self.disable_initiator_tag is not None:
993
1059
    def checker_callback(self, source, condition, connection,
994
1060
                         command):
995
1061
        """The checker has completed, so take appropriate actions."""
996
 
        self.checker_callback_tag = None
997
 
        self.checker = None
998
1062
        # Read return code from connection (see call_pipe)
999
1063
        returncode = connection.recv()
1000
1064
        connection.close()
 
1065
        if self.checker is not None:
 
1066
            self.checker.join()
 
1067
        self.checker_callback_tag = None
 
1068
        self.checker = None
1001
1069
 
1002
1070
        if returncode >= 0:
1003
1071
            self.last_checker_status = returncode
1092
1160
                kwargs=popen_args)
1093
1161
            self.checker.start()
1094
1162
            self.checker_callback_tag = GLib.io_add_watch(
1095
 
                pipe[0].fileno(), GLib.IO_IN,
 
1163
                GLib.IOChannel.unix_new(pipe[0].fileno()),
 
1164
                GLib.PRIORITY_DEFAULT, GLib.IO_IN,
1096
1165
                self.checker_callback, pipe[0], command)
1097
1166
        # Re-run this periodically if run by GLib.timeout_add
1098
1167
        return True
1353
1422
                raise ValueError("Byte arrays not supported for non-"
1354
1423
                                 "'ay' signature {!r}"
1355
1424
                                 .format(prop._dbus_signature))
1356
 
            value = dbus.ByteArray(b''.join(chr(byte)
1357
 
                                            for byte in value))
 
1425
            value = dbus.ByteArray(bytes(value))
1358
1426
        prop(value)
1359
1427
 
1360
1428
    @dbus.service.method(dbus.PROPERTIES_IFACE,
1998
2066
    def Name_dbus_property(self):
1999
2067
        return dbus.String(self.name)
2000
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
 
2001
2076
    # Fingerprint - property
2002
2077
    @dbus_annotations(
2003
2078
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2158
2233
    del _interface
2159
2234
 
2160
2235
 
2161
 
class ProxyClient(object):
2162
 
    def __init__(self, child_pipe, fpr, address):
 
2236
class ProxyClient:
 
2237
    def __init__(self, child_pipe, key_id, fpr, address):
2163
2238
        self._pipe = child_pipe
2164
 
        self._pipe.send(('init', fpr, address))
 
2239
        self._pipe.send(('init', key_id, fpr, address))
2165
2240
        if not self._pipe.recv():
2166
 
            raise KeyError(fpr)
 
2241
            raise KeyError(key_id or fpr)
2167
2242
 
2168
2243
    def __getattribute__(self, name):
2169
2244
        if name == '_pipe':
2236
2311
 
2237
2312
            approval_required = False
2238
2313
            try:
2239
 
                try:
2240
 
                    fpr = self.fingerprint(
2241
 
                        self.peer_certificate(session))
2242
 
                except (TypeError, gnutls.Error) as error:
2243
 
                    logger.warning("Bad certificate: %s", error)
2244
 
                    return
2245
 
                logger.debug("Fingerprint: %s", fpr)
2246
 
 
2247
 
                try:
2248
 
                    client = ProxyClient(child_pipe, fpr,
 
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,
2249
2336
                                         self.client_address)
2250
2337
                except KeyError:
2251
2338
                    return
2328
2415
 
2329
2416
    @staticmethod
2330
2417
    def peer_certificate(session):
2331
 
        "Return the peer's OpenPGP certificate as a bytestring"
2332
 
        # If not an OpenPGP certificate...
2333
 
        if (gnutls.certificate_type_get(session._c_object)
2334
 
            != gnutls.CRT_OPENPGP):
 
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)
2335
2432
            # ...return invalid data
2336
2433
            return b""
2337
2434
        list_size = ctypes.c_uint(1)
2345
2442
        return ctypes.string_at(cert.data, cert.size)
2346
2443
 
2347
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
2348
2479
    def fingerprint(openpgp):
2349
2480
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2350
2481
        # New GnuTLS "datum" with the OpenPGP public key
2364
2495
                                       ctypes.byref(crtverify))
2365
2496
        if crtverify.value != 0:
2366
2497
            gnutls.openpgp_crt_deinit(crt)
2367
 
            raise gnutls.CertificateSecurityError("Verify failed")
 
2498
            raise gnutls.CertificateSecurityError(code
 
2499
                                                  =crtverify.value)
2368
2500
        # New buffer for the fingerprint
2369
2501
        buf = ctypes.create_string_buffer(20)
2370
2502
        buf_len = ctypes.c_size_t()
2380
2512
        return hex_fpr
2381
2513
 
2382
2514
 
2383
 
class MultiprocessingMixIn(object):
 
2515
class MultiprocessingMixIn:
2384
2516
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2385
2517
 
2386
2518
    def sub_process_main(self, request, address):
2398
2530
        return proc
2399
2531
 
2400
2532
 
2401
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2533
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2402
2534
    """ adds a pipe to the MixIn """
2403
2535
 
2404
2536
    def process_request(self, request, client_address):
2419
2551
 
2420
2552
 
2421
2553
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2422
 
                     socketserver.TCPServer, object):
 
2554
                     socketserver.TCPServer):
2423
2555
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2424
2556
 
2425
2557
    Attributes:
2498
2630
                    raise
2499
2631
        # Only bind(2) the socket if we really need to.
2500
2632
        if self.server_address[0] or self.server_address[1]:
 
2633
            if self.server_address[1]:
 
2634
                self.allow_reuse_address = True
2501
2635
            if not self.server_address[0]:
2502
2636
                if self.address_family == socket.AF_INET6:
2503
2637
                    any_address = "::"  # in6addr_any
2556
2690
    def add_pipe(self, parent_pipe, proc):
2557
2691
        # Call "handle_ipc" for both data and EOF events
2558
2692
        GLib.io_add_watch(
2559
 
            parent_pipe.fileno(),
2560
 
            GLib.IO_IN | GLib.IO_HUP,
 
2693
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2694
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2561
2695
            functools.partial(self.handle_ipc,
2562
2696
                              parent_pipe=parent_pipe,
2563
2697
                              proc=proc))
2577
2711
        command = request[0]
2578
2712
 
2579
2713
        if command == 'init':
2580
 
            fpr = request[1]
2581
 
            address = request[2]
 
2714
            key_id = request[1].decode("ascii")
 
2715
            fpr = request[2].decode("ascii")
 
2716
            address = request[3]
2582
2717
 
2583
2718
            for c in self.clients.values():
2584
 
                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:
2585
2725
                    client = c
2586
2726
                    break
2587
2727
            else:
2588
 
                logger.info("Client not found for fingerprint: %s, ad"
2589
 
                            "dress: %s", fpr, address)
 
2728
                logger.info("Client not found for key ID: %s, address"
 
2729
                            ": %s", key_id or fpr, address)
2590
2730
                if self.use_dbus:
2591
2731
                    # Emit D-Bus signal
2592
 
                    mandos_dbus_service.ClientNotFound(fpr,
 
2732
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
2593
2733
                                                       address[0])
2594
2734
                parent_pipe.send(False)
2595
2735
                return False
2596
2736
 
2597
2737
            GLib.io_add_watch(
2598
 
                parent_pipe.fileno(),
2599
 
                GLib.IO_IN | GLib.IO_HUP,
 
2738
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2739
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2600
2740
                functools.partial(self.handle_ipc,
2601
2741
                                  parent_pipe=parent_pipe,
2602
2742
                                  proc=proc,
2617
2757
        if command == 'getattr':
2618
2758
            attrname = request[1]
2619
2759
            if isinstance(client_object.__getattribute__(attrname),
2620
 
                          collections.Callable):
 
2760
                          collections.abc.Callable):
2621
2761
                parent_pipe.send(('function', ))
2622
2762
            else:
2623
2763
                parent_pipe.send((
2634
2774
def rfc3339_duration_to_delta(duration):
2635
2775
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2636
2776
 
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)
 
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
2651
2791
    """
2652
2792
 
2653
2793
    # Parsing an RFC 3339 duration with regular expressions is not
2733
2873
def string_to_delta(interval):
2734
2874
    """Parse a string and return a datetime.timedelta
2735
2875
 
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)
 
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
2748
2888
    """
2749
2889
 
2750
2890
    try:
2852
2992
 
2853
2993
    options = parser.parse_args()
2854
2994
 
2855
 
    if options.check:
2856
 
        import doctest
2857
 
        fail_count, test_count = doctest.testmod()
2858
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2859
 
 
2860
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")
2861
3002
    server_defaults = {"interface": "",
2862
3003
                       "address": "",
2863
3004
                       "port": "",
2864
3005
                       "debug": "False",
2865
 
                       "priority":
2866
 
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2867
 
                       ":+SIGN-DSA-SHA256",
 
3006
                       "priority": priority,
2868
3007
                       "servicename": "Mandos",
2869
3008
                       "use_dbus": "True",
2870
3009
                       "use_ipv6": "True",
2875
3014
                       "foreground": "False",
2876
3015
                       "zeroconf": "True",
2877
3016
                       }
 
3017
    del priority
2878
3018
 
2879
3019
    # Parse config file for server-global settings
2880
 
    server_config = configparser.SafeConfigParser(server_defaults)
 
3020
    server_config = configparser.ConfigParser(server_defaults)
2881
3021
    del server_defaults
2882
3022
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
2883
 
    # Convert the SafeConfigParser object to a dict
 
3023
    # Convert the ConfigParser object to a dict
2884
3024
    server_settings = server_config.defaults()
2885
3025
    # Use the appropriate methods on the non-string config options
2886
3026
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2958
3098
                                  server_settings["servicename"])))
2959
3099
 
2960
3100
    # Parse config file with clients
2961
 
    client_config = configparser.SafeConfigParser(Client
2962
 
                                                  .client_defaults)
 
3101
    client_config = configparser.ConfigParser(Client.client_defaults)
2963
3102
    client_config.read(os.path.join(server_settings["configdir"],
2964
3103
                                    "clients.conf"))
2965
3104
 
3036
3175
        # Close all input and output, do double fork, etc.
3037
3176
        daemon()
3038
3177
 
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()
 
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()
3042
3182
 
3043
3183
    global main_loop
3044
3184
    # From the Avahi example code
3120
3260
                             if isinstance(s, bytes)
3121
3261
                             else s) for s in
3122
3262
                            value["client_structure"]]
3123
 
                        # .name & .host
3124
 
                        for k in ("name", "host"):
 
3263
                        # .name, .host, and .checker_command
 
3264
                        for k in ("name", "host", "checker_command"):
3125
3265
                            if isinstance(value[k], bytes):
3126
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"] = ""
3127
3271
                    #  old_client_settings
3128
3272
                    # .keys()
3129
3273
                    old_client_settings = {
3133
3277
                        for key, value in
3134
3278
                        bytes_old_client_settings.items()}
3135
3279
                    del bytes_old_client_settings
3136
 
                    # .host
 
3280
                    # .host and .checker_command
3137
3281
                    for value in old_client_settings.values():
3138
 
                        if isinstance(value["host"], bytes):
3139
 
                            value["host"] = (value["host"]
3140
 
                                             .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"))
3141
3286
            os.remove(stored_state_path)
3142
3287
        except IOError as e:
3143
3288
            if e.errno == errno.ENOENT:
3266
3411
                pass
3267
3412
 
3268
3413
            @dbus.service.signal(_interface, signature="ss")
3269
 
            def ClientNotFound(self, fingerprint, address):
 
3414
            def ClientNotFound(self, key_id, address):
3270
3415
                "D-Bus signal"
3271
3416
                pass
3272
3417
 
3468
3613
                sys.exit(1)
3469
3614
            # End of Avahi example code
3470
3615
 
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))
 
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))
3475
3621
 
3476
3622
        logger.debug("Starting main loop")
3477
3623
        main_loop.run()
3487
3633
    # Must run before the D-Bus bus name gets deregistered
3488
3634
    cleanup()
3489
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
3490
3652
 
3491
3653
if __name__ == '__main__':
3492
 
    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()