/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-ctl

  • Committer: Teddy Hogeborn
  • Date: 2019-02-10 08:41:14 UTC
  • Revision ID: teddy@recompile.se-20190210084114-u91mijrxtifvzra5
Bug fix: Only create TLS key with certtool, and read correct key file

* debian/mandos-client.postinst (create_keys): Remove any bad keys
                                               created by 1.8.0-1.
                                               Only create TLS keys if
                                               certtool succeeds.
* debian/mandos.postinst (configure): Remove any bad keys from
                                      clients.conf, and inform the
                                      user if any were found.
* debian/mandos.templates (mandos/removed_bad_key_ids): New message.
* mandos (MandosServer.handle_ipc): Do not trust a key_id with a known
                                    bad key ID.
* mandos-keygen (keygen): Only create TLS keys if certtool succeeds.
  (password): Bug fix: Generate key_id correctly, and only output
              key_id if TLS key exists.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
2
# -*- mode: python; coding: utf-8 -*-
3
 
 
3
#
4
4
# Mandos Monitor - Control and monitor the Mandos server
5
 
6
 
# Copyright © 2008-2016 Teddy Hogeborn
7
 
# Copyright © 2008-2016 Björn Påhlsson
8
 
9
 
# This program is free software: you can redistribute it and/or modify
10
 
# it under the terms of the GNU General Public License as published by
 
5
#
 
6
# Copyright © 2008-2019 Teddy Hogeborn
 
7
# Copyright © 2008-2019 Björn Påhlsson
 
8
#
 
9
# This file is part of Mandos.
 
10
#
 
11
# Mandos is free software: you can redistribute it and/or modify it
 
12
# under the terms of the GNU General Public License as published by
11
13
# the Free Software Foundation, either version 3 of the License, or
12
14
# (at your option) any later version.
13
15
#
14
 
#     This program is distributed in the hope that it will be useful,
15
 
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
 
16
#     Mandos is distributed in the hope that it will be useful, but
 
17
#     WITHOUT ANY WARRANTY; without even the implied warranty of
16
18
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
19
#     GNU General Public License for more details.
18
 
 
20
#
19
21
# You should have received a copy of the GNU General Public License
20
 
# along with this program.  If not, see
21
 
# <http://www.gnu.org/licenses/>.
22
 
 
22
# along with Mandos.  If not, see <http://www.gnu.org/licenses/>.
 
23
#
23
24
# Contact the authors at <mandos@recompile.se>.
24
 
 
25
#
25
26
 
26
27
from __future__ import (division, absolute_import, print_function,
27
28
                        unicode_literals)
38
39
import re
39
40
import os
40
41
import collections
41
 
import doctest
42
42
import json
43
43
 
44
44
import dbus
58
58
    "Interval": "Interval",
59
59
    "Host": "Host",
60
60
    "Fingerprint": "Fingerprint",
 
61
    "KeyID": "Key ID",
61
62
    "CheckerRunning": "Check Is Running",
62
63
    "LastEnabled": "Last Enabled",
63
64
    "ApprovalPending": "Approval Is Pending",
65
66
    "ApprovalDelay": "Approval Delay",
66
67
    "ApprovalDuration": "Approval Duration",
67
68
    "Checker": "Checker",
68
 
    "ExtendedTimeout": "Extended Timeout"
 
69
    "ExtendedTimeout": "Extended Timeout",
 
70
    "Expires": "Expires",
 
71
    "LastCheckerStatus": "Last Checker Status",
69
72
}
70
73
defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK")
71
74
domain = "se.recompile"
73
76
server_path = "/"
74
77
server_interface = domain + ".Mandos"
75
78
client_interface = domain + ".Mandos.Client"
76
 
version = "1.7.10"
 
79
version = "1.8.0"
77
80
 
78
81
 
79
82
try:
81
84
except AttributeError:
82
85
    dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
83
86
 
 
87
 
84
88
def milliseconds_to_string(ms):
85
89
    td = datetime.timedelta(0, 0, 0, ms)
86
 
    return ("{days}{hours:02}:{minutes:02}:{seconds:02}".format(
87
 
        days = "{}T".format(td.days) if td.days else "",
88
 
        hours = td.seconds // 3600,
89
 
        minutes = (td.seconds % 3600) // 60,
90
 
        seconds = td.seconds % 60))
 
90
    return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
 
91
            .format(days="{}T".format(td.days) if td.days else "",
 
92
                    hours=td.seconds // 3600,
 
93
                    minutes=(td.seconds % 3600) // 60,
 
94
                    seconds=td.seconds % 60))
91
95
 
92
96
 
93
97
def rfc3339_duration_to_delta(duration):
94
98
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
95
 
    
 
99
 
96
100
    >>> rfc3339_duration_to_delta("P7D")
97
101
    datetime.timedelta(7)
98
102
    >>> rfc3339_duration_to_delta("PT60S")
108
112
    >>> rfc3339_duration_to_delta("P1DT3M20S")
109
113
    datetime.timedelta(1, 200)
110
114
    """
111
 
    
 
115
 
112
116
    # Parsing an RFC 3339 duration with regular expressions is not
113
117
    # possible - there would have to be multiple places for the same
114
118
    # values, like seconds.  The current code, while more esoteric, is
115
119
    # cleaner without depending on a parsing library.  If Python had a
116
120
    # built-in library for parsing we would use it, but we'd like to
117
121
    # avoid excessive use of external libraries.
118
 
    
 
122
 
119
123
    # New type for defining tokens, syntax, and semantics all-in-one
120
124
    Token = collections.namedtuple("Token", (
121
125
        "regexp",  # To match token; if "value" is not None, must have
154
158
                           frozenset((token_year, token_month,
155
159
                                      token_day, token_time,
156
160
                                      token_week)))
157
 
    # Define starting values
158
 
    value = datetime.timedelta() # Value so far
 
161
    # Define starting values:
 
162
    # Value so far
 
163
    value = datetime.timedelta()
159
164
    found_token = None
160
 
    followers = frozenset((token_duration, )) # Following valid tokens
161
 
    s = duration                # String left to parse
 
165
    # Following valid tokens
 
166
    followers = frozenset((token_duration, ))
 
167
    # String left to parse
 
168
    s = duration
162
169
    # Loop until end token is found
163
170
    while found_token is not token_end:
164
171
        # Search for any currently valid tokens
188
195
 
189
196
def string_to_delta(interval):
190
197
    """Parse a string and return a datetime.timedelta
191
 
    
 
198
 
192
199
    >>> string_to_delta('7d')
193
200
    datetime.timedelta(7)
194
201
    >>> string_to_delta('60s')
202
209
    >>> string_to_delta('5m 30s')
203
210
    datetime.timedelta(0, 330)
204
211
    """
205
 
    
 
212
 
206
213
    try:
207
214
        return rfc3339_duration_to_delta(interval)
208
215
    except ValueError:
209
216
        pass
210
 
    
 
217
 
211
218
    value = datetime.timedelta(0)
212
219
    regexp = re.compile(r"(\d+)([dsmhw]?)")
213
 
    
 
220
 
214
221
    for num, suffix in regexp.findall(interval):
215
222
        if suffix == "d":
216
223
            value += datetime.timedelta(int(num))
235
242
                       "ApprovalDuration", "ExtendedTimeout"):
236
243
            return milliseconds_to_string(value)
237
244
        return str(value)
238
 
    
 
245
 
239
246
    # Create format string to print table rows
240
247
    format_string = " ".join("{{{key}:{width}}}".format(
241
 
        width = max(len(tablewords[key]),
242
 
                    max(len(valuetostring(client[key], key))
243
 
                        for client in clients)),
244
 
        key = key)
 
248
        width=max(len(tablewords[key]),
 
249
                  max(len(valuetostring(client[key], key))
 
250
                      for client in clients)),
 
251
        key=key)
245
252
                             for key in keywords)
246
253
    # Print header line
247
254
    print(format_string.format(**tablewords))
248
255
    for client in clients:
249
 
        print(format_string.format(**{
250
 
            key: valuetostring(client[key], key)
251
 
            for key in keywords }))
 
256
        print(format_string
 
257
              .format(**{key: valuetostring(client[key], key)
 
258
                         for key in keywords}))
252
259
 
253
260
 
254
261
def has_actions(options):
275
282
def main():
276
283
    parser = argparse.ArgumentParser()
277
284
    parser.add_argument("--version", action="version",
278
 
                        version = "%(prog)s {}".format(version),
 
285
                        version="%(prog)s {}".format(version),
279
286
                        help="show version number and exit")
280
287
    parser.add_argument("-a", "--all", action="store_true",
281
288
                        help="Select all clients")
327
334
                        help="Run self-test")
328
335
    parser.add_argument("client", nargs="*", help="Client name")
329
336
    options = parser.parse_args()
330
 
    
 
337
 
331
338
    if has_actions(options) and not (options.client or options.all):
332
339
        parser.error("Options require clients names or --all.")
333
340
    if options.verbose and has_actions(options):
334
341
        parser.error("--verbose can only be used alone.")
335
 
    if options.dump_json and (options.verbose or has_actions(options)):
 
342
    if options.dump_json and (options.verbose
 
343
                              or has_actions(options)):
336
344
        parser.error("--dump-json can only be used alone.")
337
345
    if options.all and not has_actions(options):
338
346
        parser.error("--all requires an action.")
339
 
    
 
347
 
340
348
    if options.check:
 
349
        import doctest
341
350
        fail_count, test_count = doctest.testmod()
342
351
        sys.exit(os.EX_OK if fail_count == 0 else 1)
343
 
    
 
352
 
344
353
    try:
345
354
        bus = dbus.SystemBus()
346
355
        mandos_dbus_objc = bus.get_object(busname, server_path)
347
356
    except dbus.exceptions.DBusException:
348
357
        print("Could not connect to Mandos server", file=sys.stderr)
349
358
        sys.exit(1)
350
 
    
 
359
 
351
360
    mandos_serv = dbus.Interface(mandos_dbus_objc,
352
 
                                 dbus_interface = server_interface)
 
361
                                 dbus_interface=server_interface)
353
362
    mandos_serv_object_manager = dbus.Interface(
354
 
        mandos_dbus_objc, dbus_interface = dbus.OBJECT_MANAGER_IFACE)
355
 
    
356
 
    #block stderr since dbus library prints to stderr
 
363
        mandos_dbus_objc, dbus_interface=dbus.OBJECT_MANAGER_IFACE)
 
364
 
 
365
    # block stderr since dbus library prints to stderr
357
366
    null = os.open(os.path.devnull, os.O_RDWR)
358
367
    stderrcopy = os.dup(sys.stderr.fileno())
359
368
    os.dup2(null, sys.stderr.fileno())
360
369
    os.close(null)
361
370
    try:
362
371
        try:
363
 
            mandos_clients = { path: ifs_and_props[client_interface]
364
 
                               for path, ifs_and_props in
365
 
                               mandos_serv_object_manager
366
 
                               .GetManagedObjects().items()
367
 
                               if client_interface in ifs_and_props }
 
372
            mandos_clients = {path: ifs_and_props[client_interface]
 
373
                              for path, ifs_and_props in
 
374
                              mandos_serv_object_manager
 
375
                              .GetManagedObjects().items()
 
376
                              if client_interface in ifs_and_props}
368
377
        finally:
369
 
            #restore stderr
 
378
            # restore stderr
370
379
            os.dup2(stderrcopy, sys.stderr.fileno())
371
380
            os.close(stderrcopy)
372
381
    except dbus.exceptions.DBusException as e:
373
 
        print("Access denied: Accessing mandos server through D-Bus: {}"
374
 
              .format(e), file=sys.stderr)
 
382
        print("Access denied: "
 
383
              "Accessing mandos server through D-Bus: {}".format(e),
 
384
              file=sys.stderr)
375
385
        sys.exit(1)
376
 
    
 
386
 
377
387
    # Compile dict of (clients: properties) to process
378
 
    clients={}
379
 
    
 
388
    clients = {}
 
389
 
380
390
    if options.all or not options.client:
381
 
        clients = { bus.get_object(busname, path): properties
382
 
                    for path, properties in mandos_clients.items() }
 
391
        clients = {bus.get_object(busname, path): properties
 
392
                   for path, properties in mandos_clients.items()}
383
393
    else:
384
394
        for name in options.client:
385
395
            for path, client in mandos_clients.items():
391
401
                print("Client not found on server: {!r}"
392
402
                      .format(name), file=sys.stderr)
393
403
                sys.exit(1)
394
 
    
 
404
 
395
405
    if not has_actions(options) and clients:
396
406
        if options.verbose or options.dump_json:
397
407
            keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
398
 
                        "Created", "Interval", "Host", "Fingerprint",
399
 
                        "CheckerRunning", "LastEnabled",
400
 
                        "ApprovalPending", "ApprovedByDefault",
401
 
                        "LastApprovalRequest", "ApprovalDelay",
402
 
                        "ApprovalDuration", "Checker",
403
 
                        "ExtendedTimeout")
 
408
                        "Created", "Interval", "Host", "KeyID",
 
409
                        "Fingerprint", "CheckerRunning",
 
410
                        "LastEnabled", "ApprovalPending",
 
411
                        "ApprovedByDefault", "LastApprovalRequest",
 
412
                        "ApprovalDelay", "ApprovalDuration",
 
413
                        "Checker", "ExtendedTimeout", "Expires",
 
414
                        "LastCheckerStatus")
404
415
        else:
405
416
            keywords = defaultkeywords
406
 
        
 
417
 
407
418
        if options.dump_json:
408
419
            json.dump({client["Name"]: {key:
409
420
                                        bool(client[key])
410
421
                                        if isinstance(client[key],
411
422
                                                      dbus.Boolean)
412
423
                                        else client[key]
413
 
                                        for key in keywords }
414
 
                       for client in clients.values() },
415
 
                      fp = sys.stdout, indent = 4,
416
 
                      separators = (',', ': '))
 
424
                                        for key in keywords}
 
425
                       for client in clients.values()},
 
426
                      fp=sys.stdout, indent=4,
 
427
                      separators=(',', ': '))
417
428
            print()
418
429
        else:
419
430
            print_clients(clients.values(), keywords)
420
431
    else:
421
432
        # Process each client in the list by all selected options
422
433
        for client in clients:
423
 
            
 
434
 
424
435
            def set_client_prop(prop, value):
425
436
                """Set a Client D-Bus property"""
426
437
                client.Set(client_interface, prop, value,
427
438
                           dbus_interface=dbus.PROPERTIES_IFACE)
428
 
            
 
439
 
429
440
            def set_client_prop_ms(prop, value):
430
441
                """Set a Client D-Bus property, converted
431
442
                from a string to milliseconds."""
432
443
                set_client_prop(prop,
433
444
                                string_to_delta(value).total_seconds()
434
445
                                * 1000)
435
 
            
 
446
 
436
447
            if options.remove:
437
448
                mandos_serv.RemoveClient(client.__dbus_object_path__)
438
449
            if options.enable:
446
457
            if options.stop_checker:
447
458
                set_client_prop("CheckerRunning", dbus.Boolean(False))
448
459
            if options.is_enabled:
449
 
                sys.exit(0 if client.Get(client_interface,
450
 
                                         "Enabled",
451
 
                                         dbus_interface=
452
 
                                         dbus.PROPERTIES_IFACE)
453
 
                         else 1)
 
460
                if client.Get(client_interface, "Enabled",
 
461
                              dbus_interface=dbus.PROPERTIES_IFACE):
 
462
                    sys.exit(0)
 
463
                else:
 
464
                    sys.exit(1)
454
465
            if options.checker is not None:
455
466
                set_client_prop("Checker", options.checker)
456
467
            if options.host is not None: