/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 at recompile
  • Date: 2020-02-07 20:53:34 UTC
  • mto: This revision was merged to the branch mainline in revision 396.
  • 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-2016 Teddy Hogeborn
15
 
# Copyright © 2008-2016 Björn Påhlsson
16
 
#
17
 
# This program is free software: you can redistribute it and/or modify
18
 
# it under the terms of the GNU General Public License as published by
 
14
# Copyright © 2008-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.13"
 
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,
1460
1528
                         exc_info=error)
1461
1529
        return xmlstring
1462
1530
 
 
1531
 
1463
1532
try:
1464
1533
    dbus.OBJECT_MANAGER_IFACE
1465
1534
except AttributeError:
1997
2066
    def Name_dbus_property(self):
1998
2067
        return dbus.String(self.name)
1999
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
 
2000
2076
    # Fingerprint - property
2001
2077
    @dbus_annotations(
2002
2078
        {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
2157
2233
    del _interface
2158
2234
 
2159
2235
 
2160
 
class ProxyClient(object):
2161
 
    def __init__(self, child_pipe, fpr, address):
 
2236
class ProxyClient:
 
2237
    def __init__(self, child_pipe, key_id, fpr, address):
2162
2238
        self._pipe = child_pipe
2163
 
        self._pipe.send(('init', fpr, address))
 
2239
        self._pipe.send(('init', key_id, fpr, address))
2164
2240
        if not self._pipe.recv():
2165
 
            raise KeyError(fpr)
 
2241
            raise KeyError(key_id or fpr)
2166
2242
 
2167
2243
    def __getattribute__(self, name):
2168
2244
        if name == '_pipe':
2235
2311
 
2236
2312
            approval_required = False
2237
2313
            try:
2238
 
                try:
2239
 
                    fpr = self.fingerprint(
2240
 
                        self.peer_certificate(session))
2241
 
                except (TypeError, gnutls.Error) as error:
2242
 
                    logger.warning("Bad certificate: %s", error)
2243
 
                    return
2244
 
                logger.debug("Fingerprint: %s", fpr)
2245
 
 
2246
 
                try:
2247
 
                    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,
2248
2336
                                         self.client_address)
2249
2337
                except KeyError:
2250
2338
                    return
2327
2415
 
2328
2416
    @staticmethod
2329
2417
    def peer_certificate(session):
2330
 
        "Return the peer's OpenPGP certificate as a bytestring"
2331
 
        # If not an OpenPGP certificate...
2332
 
        if (gnutls.certificate_type_get(session._c_object)
2333
 
            != 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)
2334
2432
            # ...return invalid data
2335
2433
            return b""
2336
2434
        list_size = ctypes.c_uint(1)
2344
2442
        return ctypes.string_at(cert.data, cert.size)
2345
2443
 
2346
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
2347
2479
    def fingerprint(openpgp):
2348
2480
        "Convert an OpenPGP bytestring to a hexdigit fingerprint"
2349
2481
        # New GnuTLS "datum" with the OpenPGP public key
2363
2495
                                       ctypes.byref(crtverify))
2364
2496
        if crtverify.value != 0:
2365
2497
            gnutls.openpgp_crt_deinit(crt)
2366
 
            raise gnutls.CertificateSecurityError("Verify failed")
 
2498
            raise gnutls.CertificateSecurityError(code
 
2499
                                                  =crtverify.value)
2367
2500
        # New buffer for the fingerprint
2368
2501
        buf = ctypes.create_string_buffer(20)
2369
2502
        buf_len = ctypes.c_size_t()
2379
2512
        return hex_fpr
2380
2513
 
2381
2514
 
2382
 
class MultiprocessingMixIn(object):
 
2515
class MultiprocessingMixIn:
2383
2516
    """Like socketserver.ThreadingMixIn, but with multiprocessing"""
2384
2517
 
2385
2518
    def sub_process_main(self, request, address):
2397
2530
        return proc
2398
2531
 
2399
2532
 
2400
 
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
 
2533
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
2401
2534
    """ adds a pipe to the MixIn """
2402
2535
 
2403
2536
    def process_request(self, request, client_address):
2418
2551
 
2419
2552
 
2420
2553
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2421
 
                     socketserver.TCPServer, object):
 
2554
                     socketserver.TCPServer):
2422
2555
    """IPv6-capable TCP server.  Accepts 'None' as address and/or port
2423
2556
 
2424
2557
    Attributes:
2497
2630
                    raise
2498
2631
        # Only bind(2) the socket if we really need to.
2499
2632
        if self.server_address[0] or self.server_address[1]:
 
2633
            if self.server_address[1]:
 
2634
                self.allow_reuse_address = True
2500
2635
            if not self.server_address[0]:
2501
2636
                if self.address_family == socket.AF_INET6:
2502
2637
                    any_address = "::"  # in6addr_any
2555
2690
    def add_pipe(self, parent_pipe, proc):
2556
2691
        # Call "handle_ipc" for both data and EOF events
2557
2692
        GLib.io_add_watch(
2558
 
            parent_pipe.fileno(),
2559
 
            GLib.IO_IN | GLib.IO_HUP,
 
2693
            GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2694
            GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2560
2695
            functools.partial(self.handle_ipc,
2561
2696
                              parent_pipe=parent_pipe,
2562
2697
                              proc=proc))
2576
2711
        command = request[0]
2577
2712
 
2578
2713
        if command == 'init':
2579
 
            fpr = request[1]
2580
 
            address = request[2]
 
2714
            key_id = request[1].decode("ascii")
 
2715
            fpr = request[2].decode("ascii")
 
2716
            address = request[3]
2581
2717
 
2582
2718
            for c in self.clients.values():
2583
 
                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:
2584
2725
                    client = c
2585
2726
                    break
2586
2727
            else:
2587
 
                logger.info("Client not found for fingerprint: %s, ad"
2588
 
                            "dress: %s", fpr, address)
 
2728
                logger.info("Client not found for key ID: %s, address"
 
2729
                            ": %s", key_id or fpr, address)
2589
2730
                if self.use_dbus:
2590
2731
                    # Emit D-Bus signal
2591
 
                    mandos_dbus_service.ClientNotFound(fpr,
 
2732
                    mandos_dbus_service.ClientNotFound(key_id or fpr,
2592
2733
                                                       address[0])
2593
2734
                parent_pipe.send(False)
2594
2735
                return False
2595
2736
 
2596
2737
            GLib.io_add_watch(
2597
 
                parent_pipe.fileno(),
2598
 
                GLib.IO_IN | GLib.IO_HUP,
 
2738
                GLib.IOChannel.unix_new(parent_pipe.fileno()),
 
2739
                GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
2599
2740
                functools.partial(self.handle_ipc,
2600
2741
                                  parent_pipe=parent_pipe,
2601
2742
                                  proc=proc,
2616
2757
        if command == 'getattr':
2617
2758
            attrname = request[1]
2618
2759
            if isinstance(client_object.__getattribute__(attrname),
2619
 
                          collections.Callable):
 
2760
                          collections.abc.Callable):
2620
2761
                parent_pipe.send(('function', ))
2621
2762
            else:
2622
2763
                parent_pipe.send((
2633
2774
def rfc3339_duration_to_delta(duration):
2634
2775
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2635
2776
 
2636
 
    >>> rfc3339_duration_to_delta("P7D")
2637
 
    datetime.timedelta(7)
2638
 
    >>> rfc3339_duration_to_delta("PT60S")
2639
 
    datetime.timedelta(0, 60)
2640
 
    >>> rfc3339_duration_to_delta("PT60M")
2641
 
    datetime.timedelta(0, 3600)
2642
 
    >>> rfc3339_duration_to_delta("PT24H")
2643
 
    datetime.timedelta(1)
2644
 
    >>> rfc3339_duration_to_delta("P1W")
2645
 
    datetime.timedelta(7)
2646
 
    >>> rfc3339_duration_to_delta("PT5M30S")
2647
 
    datetime.timedelta(0, 330)
2648
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
2649
 
    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
2650
2791
    """
2651
2792
 
2652
2793
    # Parsing an RFC 3339 duration with regular expressions is not
2732
2873
def string_to_delta(interval):
2733
2874
    """Parse a string and return a datetime.timedelta
2734
2875
 
2735
 
    >>> string_to_delta('7d')
2736
 
    datetime.timedelta(7)
2737
 
    >>> string_to_delta('60s')
2738
 
    datetime.timedelta(0, 60)
2739
 
    >>> string_to_delta('60m')
2740
 
    datetime.timedelta(0, 3600)
2741
 
    >>> string_to_delta('24h')
2742
 
    datetime.timedelta(1)
2743
 
    >>> string_to_delta('1w')
2744
 
    datetime.timedelta(7)
2745
 
    >>> string_to_delta('5m 30s')
2746
 
    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
2747
2888
    """
2748
2889
 
2749
2890
    try:
2851
2992
 
2852
2993
    options = parser.parse_args()
2853
2994
 
2854
 
    if options.check:
2855
 
        import doctest
2856
 
        fail_count, test_count = doctest.testmod()
2857
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
2858
 
 
2859
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")
2860
3002
    server_defaults = {"interface": "",
2861
3003
                       "address": "",
2862
3004
                       "port": "",
2863
3005
                       "debug": "False",
2864
 
                       "priority":
2865
 
                       "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2866
 
                       ":+SIGN-DSA-SHA256",
 
3006
                       "priority": priority,
2867
3007
                       "servicename": "Mandos",
2868
3008
                       "use_dbus": "True",
2869
3009
                       "use_ipv6": "True",
2874
3014
                       "foreground": "False",
2875
3015
                       "zeroconf": "True",
2876
3016
                       }
 
3017
    del priority
2877
3018
 
2878
3019
    # Parse config file for server-global settings
2879
 
    server_config = configparser.SafeConfigParser(server_defaults)
 
3020
    server_config = configparser.ConfigParser(server_defaults)
2880
3021
    del server_defaults
2881
3022
    server_config.read(os.path.join(options.configdir, "mandos.conf"))
2882
 
    # Convert the SafeConfigParser object to a dict
 
3023
    # Convert the ConfigParser object to a dict
2883
3024
    server_settings = server_config.defaults()
2884
3025
    # Use the appropriate methods on the non-string config options
2885
 
    for option in ("debug", "use_dbus", "use_ipv6", "foreground"):
 
3026
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
 
3027
                   "foreground", "zeroconf"):
2886
3028
        server_settings[option] = server_config.getboolean("DEFAULT",
2887
3029
                                                           option)
2888
3030
    if server_settings["port"]:
2956
3098
                                  server_settings["servicename"])))
2957
3099
 
2958
3100
    # Parse config file with clients
2959
 
    client_config = configparser.SafeConfigParser(Client
2960
 
                                                  .client_defaults)
 
3101
    client_config = configparser.ConfigParser(Client.client_defaults)
2961
3102
    client_config.read(os.path.join(server_settings["configdir"],
2962
3103
                                    "clients.conf"))
2963
3104
 
3034
3175
        # Close all input and output, do double fork, etc.
3035
3176
        daemon()
3036
3177
 
3037
 
    # multiprocessing will use threads, so before we use GLib we need
3038
 
    # to inform GLib that threads will be used.
3039
 
    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()
3040
3182
 
3041
3183
    global main_loop
3042
3184
    # From the Avahi example code
3118
3260
                             if isinstance(s, bytes)
3119
3261
                             else s) for s in
3120
3262
                            value["client_structure"]]
3121
 
                        # .name & .host
3122
 
                        for k in ("name", "host"):
 
3263
                        # .name, .host, and .checker_command
 
3264
                        for k in ("name", "host", "checker_command"):
3123
3265
                            if isinstance(value[k], bytes):
3124
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"] = ""
3125
3271
                    #  old_client_settings
3126
3272
                    # .keys()
3127
3273
                    old_client_settings = {
3131
3277
                        for key, value in
3132
3278
                        bytes_old_client_settings.items()}
3133
3279
                    del bytes_old_client_settings
3134
 
                    # .host
 
3280
                    # .host and .checker_command
3135
3281
                    for value in old_client_settings.values():
3136
 
                        if isinstance(value["host"], bytes):
3137
 
                            value["host"] = (value["host"]
3138
 
                                             .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"))
3139
3286
            os.remove(stored_state_path)
3140
3287
        except IOError as e:
3141
3288
            if e.errno == errno.ENOENT:
3264
3411
                pass
3265
3412
 
3266
3413
            @dbus.service.signal(_interface, signature="ss")
3267
 
            def ClientNotFound(self, fingerprint, address):
 
3414
            def ClientNotFound(self, key_id, address):
3268
3415
                "D-Bus signal"
3269
3416
                pass
3270
3417
 
3466
3613
                sys.exit(1)
3467
3614
            # End of Avahi example code
3468
3615
 
3469
 
        GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3470
 
                          lambda *args, **kwargs:
3471
 
                          (tcp_server.handle_request
3472
 
                           (*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))
3473
3621
 
3474
3622
        logger.debug("Starting main loop")
3475
3623
        main_loop.run()
3485
3633
    # Must run before the D-Bus bus name gets deregistered
3486
3634
    cleanup()
3487
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
3488
3652
 
3489
3653
if __name__ == '__main__':
3490
 
    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()