/mandos/trunk

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

« back to all changes in this revision

Viewing changes to mandos

  • Committer: Teddy Hogeborn
  • Date: 2012-06-23 00:58:49 UTC
  • Revision ID: teddy@recompile.se-20120623005849-02wj82cng433rt2k
* clients.conf: Convert all time intervals to new RFC 3339 syntax.
* mandos: All client options for time intervals now take an RFC 3339
          duration.
  (rfc3339_duration_to_delta): New function.
  (string_to_delta): Try rfc3339_duration_to_delta first.
* mandos-clients.conf.xml (OPTIONS/timeout): Document new format.
  (EXAMPLE): Update to new interval format.
  (SEE ALSO): Reference RFC 3339.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
# "AvahiService" class, and some lines in "main".
12
12
13
13
# Everything else is
14
 
# Copyright © 2008-2014 Teddy Hogeborn
15
 
# Copyright © 2008-2014 Björn Påhlsson
 
14
# Copyright © 2008-2012 Teddy Hogeborn
 
15
# Copyright © 2008-2012 Björn Påhlsson
16
16
17
17
# This program is free software: you can redistribute it and/or modify
18
18
# it under the terms of the GNU General Public License as published by
79
79
import ctypes.util
80
80
import xml.dom.minidom
81
81
import inspect
 
82
import GnuPGInterface
82
83
 
83
84
try:
84
85
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
88
89
    except ImportError:
89
90
        SO_BINDTODEVICE = None
90
91
 
91
 
version = "1.6.3"
 
92
version = "1.6.0"
92
93
stored_state_file = "clients.pickle"
93
94
 
94
95
logger = logging.getLogger()
139
140
class PGPEngine(object):
140
141
    """A simple class for OpenPGP symmetric encryption & decryption"""
141
142
    def __init__(self):
 
143
        self.gnupg = GnuPGInterface.GnuPG()
142
144
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
143
 
        self.gnupgargs = ['--batch',
144
 
                          '--home', self.tempdir,
145
 
                          '--force-mdc',
146
 
                          '--quiet',
147
 
                          '--no-use-agent']
 
145
        self.gnupg = GnuPGInterface.GnuPG()
 
146
        self.gnupg.options.meta_interactive = False
 
147
        self.gnupg.options.homedir = self.tempdir
 
148
        self.gnupg.options.extra_args.extend(['--force-mdc',
 
149
                                              '--quiet',
 
150
                                              '--no-use-agent'])
148
151
    
149
152
    def __enter__(self):
150
153
        return self
172
175
    def password_encode(self, password):
173
176
        # Passphrase can not be empty and can not contain newlines or
174
177
        # NUL bytes.  So we prefix it and hex encode it.
175
 
        encoded = b"mandos" + binascii.hexlify(password)
176
 
        if len(encoded) > 2048:
177
 
            # GnuPG can't handle long passwords, so encode differently
178
 
            encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
179
 
                       .replace(b"\n", b"\\n")
180
 
                       .replace(b"\0", b"\\x00"))
181
 
        return encoded
 
178
        return b"mandos" + binascii.hexlify(password)
182
179
    
183
180
    def encrypt(self, data, password):
184
 
        passphrase = self.password_encode(password)
185
 
        with tempfile.NamedTemporaryFile(dir=self.tempdir
186
 
                                         ) as passfile:
187
 
            passfile.write(passphrase)
188
 
            passfile.flush()
189
 
            proc = subprocess.Popen(['gpg', '--symmetric',
190
 
                                     '--passphrase-file',
191
 
                                     passfile.name]
192
 
                                    + self.gnupgargs,
193
 
                                    stdin = subprocess.PIPE,
194
 
                                    stdout = subprocess.PIPE,
195
 
                                    stderr = subprocess.PIPE)
196
 
            ciphertext, err = proc.communicate(input = data)
197
 
        if proc.returncode != 0:
198
 
            raise PGPError(err)
 
181
        self.gnupg.passphrase = self.password_encode(password)
 
182
        with open(os.devnull, "w") as devnull:
 
183
            try:
 
184
                proc = self.gnupg.run(['--symmetric'],
 
185
                                      create_fhs=['stdin', 'stdout'],
 
186
                                      attach_fhs={'stderr': devnull})
 
187
                with contextlib.closing(proc.handles['stdin']) as f:
 
188
                    f.write(data)
 
189
                with contextlib.closing(proc.handles['stdout']) as f:
 
190
                    ciphertext = f.read()
 
191
                proc.wait()
 
192
            except IOError as e:
 
193
                raise PGPError(e)
 
194
        self.gnupg.passphrase = None
199
195
        return ciphertext
200
196
    
201
197
    def decrypt(self, data, password):
202
 
        passphrase = self.password_encode(password)
203
 
        with tempfile.NamedTemporaryFile(dir = self.tempdir
204
 
                                         ) as passfile:
205
 
            passfile.write(passphrase)
206
 
            passfile.flush()
207
 
            proc = subprocess.Popen(['gpg', '--decrypt',
208
 
                                     '--passphrase-file',
209
 
                                     passfile.name]
210
 
                                    + self.gnupgargs,
211
 
                                    stdin = subprocess.PIPE,
212
 
                                    stdout = subprocess.PIPE,
213
 
                                    stderr = subprocess.PIPE)
214
 
            decrypted_plaintext, err = proc.communicate(input
215
 
                                                        = data)
216
 
        if proc.returncode != 0:
217
 
            raise PGPError(err)
 
198
        self.gnupg.passphrase = self.password_encode(password)
 
199
        with open(os.devnull, "w") as devnull:
 
200
            try:
 
201
                proc = self.gnupg.run(['--decrypt'],
 
202
                                      create_fhs=['stdin', 'stdout'],
 
203
                                      attach_fhs={'stderr': devnull})
 
204
                with contextlib.closing(proc.handles['stdin']) as f:
 
205
                    f.write(data)
 
206
                with contextlib.closing(proc.handles['stdout']) as f:
 
207
                    decrypted_plaintext = f.read()
 
208
                proc.wait()
 
209
            except IOError as e:
 
210
                raise PGPError(e)
 
211
        self.gnupg.passphrase = None
218
212
        return decrypted_plaintext
219
213
 
220
214
 
240
234
               Used to optionally bind to the specified interface.
241
235
    name: string; Example: 'Mandos'
242
236
    type: string; Example: '_mandos._tcp'.
243
 
     See <https://www.iana.org/assignments/service-names-port-numbers>
 
237
                  See <http://www.dns-sd.org/ServiceTypes.html>
244
238
    port: integer; what port to announce
245
239
    TXT: list of strings; TXT record for the service
246
240
    domain: string; Domain to publish on, default to .local if empty.
446
440
    runtime_expansions: Allowed attributes for runtime expansion.
447
441
    expires:    datetime.datetime(); time (UTC) when a client will be
448
442
                disabled, or None
449
 
    server_settings: The server_settings dict from main()
450
443
    """
451
444
    
452
445
    runtime_expansions = ("approval_delay", "approval_duration",
527
520
        
528
521
        return settings
529
522
    
530
 
    def __init__(self, settings, name = None, server_settings=None):
 
523
    def __init__(self, settings, name = None):
531
524
        self.name = name
532
 
        if server_settings is None:
533
 
            server_settings = {}
534
 
        self.server_settings = server_settings
535
525
        # adding all client settings
536
526
        for setting, value in settings.iteritems():
537
527
            setattr(self, setting, value)
690
680
        # If a checker exists, make sure it is not a zombie
691
681
        try:
692
682
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
693
 
        except AttributeError:
694
 
            pass
695
 
        except OSError as error:
696
 
            if error.errno != errno.ECHILD:
697
 
                raise
 
683
        except (AttributeError, OSError) as error:
 
684
            if (isinstance(error, OSError)
 
685
                and error.errno != errno.ECHILD):
 
686
                raise error
698
687
        else:
699
688
            if pid:
700
689
                logger.warning("Checker was a zombie")
722
711
                # in normal mode, that is already done by daemon(),
723
712
                # and in debug mode we don't want to.  (Stdin is
724
713
                # always replaced by /dev/null.)
725
 
                # The exception is when not debugging but nevertheless
726
 
                # running in the foreground; use the previously
727
 
                # created wnull.
728
 
                popen_args = {}
729
 
                if (not self.server_settings["debug"]
730
 
                    and self.server_settings["foreground"]):
731
 
                    popen_args.update({"stdout": wnull,
732
 
                                       "stderr": wnull })
733
714
                self.checker = subprocess.Popen(command,
734
715
                                                close_fds=True,
735
 
                                                shell=True, cwd="/",
736
 
                                                **popen_args)
 
716
                                                shell=True, cwd="/")
737
717
            except OSError as error:
738
718
                logger.error("Failed to start subprocess",
739
719
                             exc_info=error)
740
 
                return True
741
720
            self.checker_callback_tag = (gobject.child_watch_add
742
721
                                         (self.checker.pid,
743
722
                                          self.checker_callback,
744
723
                                          data=command))
745
724
            # The checker may have completed before the gobject
746
725
            # watch was added.  Check for this.
747
 
            try:
748
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
749
 
            except OSError as error:
750
 
                if error.errno == errno.ECHILD:
751
 
                    # This should never happen
752
 
                    logger.error("Child process vanished",
753
 
                                 exc_info=error)
754
 
                    return True
755
 
                raise
 
726
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
756
727
            if pid:
757
728
                gobject.source_remove(self.checker_callback_tag)
758
729
                self.checker_callback(pid, status, command)
934
905
            # The byte_arrays option is not supported yet on
935
906
            # signatures other than "ay".
936
907
            if prop._dbus_signature != "ay":
937
 
                raise ValueError("Byte arrays not supported for non-"
938
 
                                 "'ay' signature {0!r}"
939
 
                                 .format(prop._dbus_signature))
 
908
                raise ValueError
940
909
            value = dbus.ByteArray(b''.join(chr(byte)
941
910
                                            for byte in value))
942
911
        prop(value)
1100
1069
                interface_names.add(alt_interface)
1101
1070
                # Is this a D-Bus signal?
1102
1071
                if getattr(attribute, "_dbus_is_signal", False):
1103
 
                    # Extract the original non-method undecorated
1104
 
                    # function by black magic
 
1072
                    # Extract the original non-method function by
 
1073
                    # black magic
1105
1074
                    nonmethod_func = (dict(
1106
1075
                            zip(attribute.func_code.co_freevars,
1107
1076
                                attribute.__closure__))["func"]
1705
1674
            logger.debug("Protocol version: %r", line)
1706
1675
            try:
1707
1676
                if int(line.strip().split()[0]) > 1:
1708
 
                    raise RuntimeError(line)
 
1677
                    raise RuntimeError
1709
1678
            except (ValueError, IndexError, RuntimeError) as error:
1710
1679
                logger.error("Unknown protocol version: %s", error)
1711
1680
                return
1918
1887
    
1919
1888
    def add_pipe(self, parent_pipe, proc):
1920
1889
        """Dummy function; override as necessary"""
1921
 
        raise NotImplementedError()
 
1890
        raise NotImplementedError
1922
1891
 
1923
1892
 
1924
1893
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2000
1969
                if self.address_family == socket.AF_INET6:
2001
1970
                    any_address = "::" # in6addr_any
2002
1971
                else:
2003
 
                    any_address = "0.0.0.0" # INADDR_ANY
 
1972
                    any_address = socket.INADDR_ANY
2004
1973
                self.server_address = (any_address,
2005
1974
                                       self.server_address[1])
2006
1975
            elif not self.server_address[1]:
2261
2230
            else:
2262
2231
                raise ValueError("Unknown suffix {0!r}"
2263
2232
                                 .format(suffix))
2264
 
        except IndexError as e:
 
2233
        except (ValueError, IndexError) as e:
2265
2234
            raise ValueError(*(e.args))
2266
2235
        timevalue += delta
2267
2236
    return timevalue
2311
2280
                        help="Run self-test")
2312
2281
    parser.add_argument("--debug", action="store_true",
2313
2282
                        help="Debug mode; run in foreground and log"
2314
 
                        " to terminal", default=None)
 
2283
                        " to terminal")
2315
2284
    parser.add_argument("--debuglevel", metavar="LEVEL",
2316
2285
                        help="Debug level for stdout output")
2317
2286
    parser.add_argument("--priority", help="GnuTLS"
2324
2293
                        " files")
2325
2294
    parser.add_argument("--no-dbus", action="store_false",
2326
2295
                        dest="use_dbus", help="Do not provide D-Bus"
2327
 
                        " system bus interface", default=None)
 
2296
                        " system bus interface")
2328
2297
    parser.add_argument("--no-ipv6", action="store_false",
2329
 
                        dest="use_ipv6", help="Do not use IPv6",
2330
 
                        default=None)
 
2298
                        dest="use_ipv6", help="Do not use IPv6")
2331
2299
    parser.add_argument("--no-restore", action="store_false",
2332
2300
                        dest="restore", help="Do not restore stored"
2333
 
                        " state", default=None)
 
2301
                        " state")
2334
2302
    parser.add_argument("--socket", type=int,
2335
2303
                        help="Specify a file descriptor to a network"
2336
2304
                        " socket to use instead of creating one")
2337
2305
    parser.add_argument("--statedir", metavar="DIR",
2338
2306
                        help="Directory to save/restore state in")
2339
2307
    parser.add_argument("--foreground", action="store_true",
2340
 
                        help="Run in foreground", default=None)
 
2308
                        help="Run in foreground")
2341
2309
    
2342
2310
    options = parser.parse_args()
2343
2311
    
2344
2312
    if options.check:
2345
2313
        import doctest
2346
 
        fail_count, test_count = doctest.testmod()
2347
 
        sys.exit(os.EX_OK if fail_count == 0 else 1)
 
2314
        doctest.testmod()
 
2315
        sys.exit()
2348
2316
    
2349
2317
    # Default values for config file for server-global settings
2350
2318
    server_defaults = { "interface": "",
2352
2320
                        "port": "",
2353
2321
                        "debug": "False",
2354
2322
                        "priority":
2355
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160",
 
2323
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2356
2324
                        "servicename": "Mandos",
2357
2325
                        "use_dbus": "True",
2358
2326
                        "use_ipv6": "True",
2402
2370
    for option in server_settings.keys():
2403
2371
        if type(server_settings[option]) is str:
2404
2372
            server_settings[option] = unicode(server_settings[option])
2405
 
    # Force all boolean options to be boolean
2406
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2407
 
                   "foreground"):
2408
 
        server_settings[option] = bool(server_settings[option])
2409
2373
    # Debug implies foreground
2410
2374
    if server_settings["debug"]:
2411
2375
        server_settings["foreground"] = True
2459
2423
                              socketfd=(server_settings["socket"]
2460
2424
                                        or None))
2461
2425
    if not foreground:
2462
 
        pidfilename = "/run/mandos.pid"
2463
 
        if not os.path.isdir("/run/."):
2464
 
            pidfilename = "/var/run/mandos.pid"
 
2426
        pidfilename = "/var/run/mandos.pid"
2465
2427
        pidfile = None
2466
2428
        try:
2467
2429
            pidfile = open(pidfilename, "w")
2484
2446
        os.setuid(uid)
2485
2447
    except OSError as error:
2486
2448
        if error.errno != errno.EPERM:
2487
 
            raise
 
2449
            raise error
2488
2450
    
2489
2451
    if debug:
2490
2452
        # Enable all possible GnuTLS debugging
2553
2515
    old_client_settings = {}
2554
2516
    clients_data = {}
2555
2517
    
2556
 
    # This is used to redirect stdout and stderr for checker processes
2557
 
    global wnull
2558
 
    wnull = open(os.devnull, "w") # A writable /dev/null
2559
 
    # Only used if server is running in foreground but not in debug
2560
 
    # mode
2561
 
    if debug or not foreground:
2562
 
        wnull.close()
2563
 
    
2564
2518
    # Get client data and settings from last running state.
2565
2519
    if server_settings["restore"]:
2566
2520
        try:
2582
2536
    
2583
2537
    with PGPEngine() as pgp:
2584
2538
        for client_name, client in clients_data.iteritems():
2585
 
            # Skip removed clients
2586
 
            if client_name not in client_settings:
2587
 
                continue
2588
 
            
2589
2539
            # Decide which value to use after restoring saved state.
2590
2540
            # We have three different values: Old config file,
2591
2541
            # new config file, and saved state.
2653
2603
    # Create all client objects
2654
2604
    for client_name, client in clients_data.iteritems():
2655
2605
        tcp_server.clients[client_name] = client_class(
2656
 
            name = client_name, settings = client,
2657
 
            server_settings = server_settings)
 
2606
            name = client_name, settings = client)
2658
2607
    
2659
2608
    if not tcp_server.clients:
2660
2609
        logger.warning("No clients defined")
2743
2692
        service.cleanup()
2744
2693
        
2745
2694
        multiprocessing.active_children()
2746
 
        wnull.close()
2747
2695
        if not (tcp_server.clients or client_settings):
2748
2696
            return
2749
2697
        
2761
2709
                # A list of attributes that can not be pickled
2762
2710
                # + secret.
2763
2711
                exclude = set(("bus", "changedstate", "secret",
2764
 
                               "checker", "server_settings"))
 
2712
                               "checker"))
2765
2713
                for name, typ in (inspect.getmembers
2766
2714
                                  (dbus.service.Object)):
2767
2715
                    exclude.add(name)
2795
2743
            else:
2796
2744
                logger.warning("Could not save persistent state:",
2797
2745
                               exc_info=e)
2798
 
                raise
 
2746
                raise e
2799
2747
        
2800
2748
        # Delete all clients, and settings from config
2801
2749
        while tcp_server.clients: