/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-17 22:26:40 UTC
  • mfrom: (237.9.1 release)
  • mto: This revision was merged to the branch mainline in revision 607.
  • Revision ID: teddy@recompile.se-20120617222640-49scfn0husnrr5bi
Tags: version-1.6.0-1
* Makefile (version): Changed to "1.6.0".
* NEWS (Version 1.6.0): New entry.
* debian/changelog (1.6.0-1): - '' -

Show diffs side-by-side

added added

removed removed

Lines of Context:
68
68
import binascii
69
69
import tempfile
70
70
import itertools
71
 
import collections
72
71
 
73
72
import dbus
74
73
import dbus.service
79
78
import ctypes.util
80
79
import xml.dom.minidom
81
80
import inspect
 
81
import GnuPGInterface
82
82
 
83
83
try:
84
84
    SO_BINDTODEVICE = socket.SO_BINDTODEVICE
139
139
class PGPEngine(object):
140
140
    """A simple class for OpenPGP symmetric encryption & decryption"""
141
141
    def __init__(self):
 
142
        self.gnupg = GnuPGInterface.GnuPG()
142
143
        self.tempdir = tempfile.mkdtemp(prefix="mandos-")
143
 
        self.gnupgargs = ['--batch',
144
 
                          '--home', self.tempdir,
145
 
                          '--force-mdc',
146
 
                          '--quiet',
147
 
                          '--no-use-agent']
 
144
        self.gnupg = GnuPGInterface.GnuPG()
 
145
        self.gnupg.options.meta_interactive = False
 
146
        self.gnupg.options.homedir = self.tempdir
 
147
        self.gnupg.options.extra_args.extend(['--force-mdc',
 
148
                                              '--quiet',
 
149
                                              '--no-use-agent'])
148
150
    
149
151
    def __enter__(self):
150
152
        return self
175
177
        return b"mandos" + binascii.hexlify(password)
176
178
    
177
179
    def encrypt(self, data, password):
178
 
        passphrase = self.password_encode(password)
179
 
        with tempfile.NamedTemporaryFile(dir=self.tempdir
180
 
                                         ) as passfile:
181
 
            passfile.write(passphrase)
182
 
            passfile.flush()
183
 
            proc = subprocess.Popen(['gpg', '--symmetric',
184
 
                                     '--passphrase-file',
185
 
                                     passfile.name]
186
 
                                    + self.gnupgargs,
187
 
                                    stdin = subprocess.PIPE,
188
 
                                    stdout = subprocess.PIPE,
189
 
                                    stderr = subprocess.PIPE)
190
 
            ciphertext, err = proc.communicate(input = data)
191
 
        if proc.returncode != 0:
192
 
            raise PGPError(err)
 
180
        self.gnupg.passphrase = self.password_encode(password)
 
181
        with open(os.devnull, "w") as devnull:
 
182
            try:
 
183
                proc = self.gnupg.run(['--symmetric'],
 
184
                                      create_fhs=['stdin', 'stdout'],
 
185
                                      attach_fhs={'stderr': devnull})
 
186
                with contextlib.closing(proc.handles['stdin']) as f:
 
187
                    f.write(data)
 
188
                with contextlib.closing(proc.handles['stdout']) as f:
 
189
                    ciphertext = f.read()
 
190
                proc.wait()
 
191
            except IOError as e:
 
192
                raise PGPError(e)
 
193
        self.gnupg.passphrase = None
193
194
        return ciphertext
194
195
    
195
196
    def decrypt(self, data, password):
196
 
        passphrase = self.password_encode(password)
197
 
        with tempfile.NamedTemporaryFile(dir = self.tempdir
198
 
                                         ) as passfile:
199
 
            passfile.write(passphrase)
200
 
            passfile.flush()
201
 
            proc = subprocess.Popen(['gpg', '--decrypt',
202
 
                                     '--passphrase-file',
203
 
                                     passfile.name]
204
 
                                    + self.gnupgargs,
205
 
                                    stdin = subprocess.PIPE,
206
 
                                    stdout = subprocess.PIPE,
207
 
                                    stderr = subprocess.PIPE)
208
 
            decrypted_plaintext, err = proc.communicate(input
209
 
                                                        = data)
210
 
        if proc.returncode != 0:
211
 
            raise PGPError(err)
 
197
        self.gnupg.passphrase = self.password_encode(password)
 
198
        with open(os.devnull, "w") as devnull:
 
199
            try:
 
200
                proc = self.gnupg.run(['--decrypt'],
 
201
                                      create_fhs=['stdin', 'stdout'],
 
202
                                      attach_fhs={'stderr': devnull})
 
203
                with contextlib.closing(proc.handles['stdin']) as f:
 
204
                    f.write(data)
 
205
                with contextlib.closing(proc.handles['stdout']) as f:
 
206
                    decrypted_plaintext = f.read()
 
207
                proc.wait()
 
208
            except IOError as e:
 
209
                raise PGPError(e)
 
210
        self.gnupg.passphrase = None
212
211
        return decrypted_plaintext
213
212
 
214
213
 
234
233
               Used to optionally bind to the specified interface.
235
234
    name: string; Example: 'Mandos'
236
235
    type: string; Example: '_mandos._tcp'.
237
 
     See <https://www.iana.org/assignments/service-names-port-numbers>
 
236
                  See <http://www.dns-sd.org/ServiceTypes.html>
238
237
    port: integer; what port to announce
239
238
    TXT: list of strings; TXT record for the service
240
239
    domain: string; Domain to publish on, default to .local if empty.
440
439
    runtime_expansions: Allowed attributes for runtime expansion.
441
440
    expires:    datetime.datetime(); time (UTC) when a client will be
442
441
                disabled, or None
443
 
    server_settings: The server_settings dict from main()
444
442
    """
445
443
    
446
444
    runtime_expansions = ("approval_delay", "approval_duration",
448
446
                          "fingerprint", "host", "interval",
449
447
                          "last_approval_request", "last_checked_ok",
450
448
                          "last_enabled", "name", "timeout")
451
 
    client_defaults = { "timeout": "PT5M",
452
 
                        "extended_timeout": "PT15M",
453
 
                        "interval": "PT2M",
 
449
    client_defaults = { "timeout": "5m",
 
450
                        "extended_timeout": "15m",
 
451
                        "interval": "2m",
454
452
                        "checker": "fping -q -- %%(host)s",
455
453
                        "host": "",
456
 
                        "approval_delay": "PT0S",
457
 
                        "approval_duration": "PT1S",
 
454
                        "approval_delay": "0s",
 
455
                        "approval_duration": "1s",
458
456
                        "approved_by_default": "True",
459
457
                        "enabled": "True",
460
458
                        }
521
519
        
522
520
        return settings
523
521
    
524
 
    def __init__(self, settings, name = None, server_settings=None):
 
522
    def __init__(self, settings, name = None):
525
523
        self.name = name
526
 
        if server_settings is None:
527
 
            server_settings = {}
528
 
        self.server_settings = server_settings
529
524
        # adding all client settings
530
525
        for setting, value in settings.iteritems():
531
526
            setattr(self, setting, value)
715
710
                # in normal mode, that is already done by daemon(),
716
711
                # and in debug mode we don't want to.  (Stdin is
717
712
                # always replaced by /dev/null.)
718
 
                # The exception is when not debugging but nevertheless
719
 
                # running in the foreground; use the previously
720
 
                # created wnull.
721
 
                popen_args = {}
722
 
                if (not self.server_settings["debug"]
723
 
                    and self.server_settings["foreground"]):
724
 
                    popen_args.update({"stdout": wnull,
725
 
                                       "stderr": wnull })
726
713
                self.checker = subprocess.Popen(command,
727
714
                                                close_fds=True,
728
 
                                                shell=True, cwd="/",
729
 
                                                **popen_args)
 
715
                                                shell=True, cwd="/")
730
716
            except OSError as error:
731
717
                logger.error("Failed to start subprocess",
732
718
                             exc_info=error)
733
 
                return True
734
719
            self.checker_callback_tag = (gobject.child_watch_add
735
720
                                         (self.checker.pid,
736
721
                                          self.checker_callback,
737
722
                                          data=command))
738
723
            # The checker may have completed before the gobject
739
724
            # watch was added.  Check for this.
740
 
            try:
741
 
                pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
742
 
            except OSError as error:
743
 
                if error.errno == errno.ECHILD:
744
 
                    # This should never happen
745
 
                    logger.error("Child process vanished",
746
 
                                 exc_info=error)
747
 
                    return True
748
 
                raise
 
725
            pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
749
726
            if pid:
750
727
                gobject.source_remove(self.checker_callback_tag)
751
728
                self.checker_callback(pid, status, command)
1091
1068
                interface_names.add(alt_interface)
1092
1069
                # Is this a D-Bus signal?
1093
1070
                if getattr(attribute, "_dbus_is_signal", False):
1094
 
                    # Extract the original non-method undecorated
1095
 
                    # function by black magic
 
1071
                    # Extract the original non-method function by
 
1072
                    # black magic
1096
1073
                    nonmethod_func = (dict(
1097
1074
                            zip(attribute.func_code.co_freevars,
1098
1075
                                attribute.__closure__))["func"]
1991
1968
                if self.address_family == socket.AF_INET6:
1992
1969
                    any_address = "::" # in6addr_any
1993
1970
                else:
1994
 
                    any_address = "0.0.0.0" # INADDR_ANY
 
1971
                    any_address = socket.INADDR_ANY
1995
1972
                self.server_address = (any_address,
1996
1973
                                       self.server_address[1])
1997
1974
            elif not self.server_address[1]:
2113
2090
        return True
2114
2091
 
2115
2092
 
2116
 
def rfc3339_duration_to_delta(duration):
2117
 
    """Parse an RFC 3339 "duration" and return a datetime.timedelta
2118
 
    
2119
 
    >>> rfc3339_duration_to_delta("P7D")
2120
 
    datetime.timedelta(7)
2121
 
    >>> rfc3339_duration_to_delta("PT60S")
2122
 
    datetime.timedelta(0, 60)
2123
 
    >>> rfc3339_duration_to_delta("PT60M")
2124
 
    datetime.timedelta(0, 3600)
2125
 
    >>> rfc3339_duration_to_delta("PT24H")
2126
 
    datetime.timedelta(1)
2127
 
    >>> rfc3339_duration_to_delta("P1W")
2128
 
    datetime.timedelta(7)
2129
 
    >>> rfc3339_duration_to_delta("PT5M30S")
2130
 
    datetime.timedelta(0, 330)
2131
 
    >>> rfc3339_duration_to_delta("P1DT3M20S")
2132
 
    datetime.timedelta(1, 200)
2133
 
    """
2134
 
    
2135
 
    # Parsing an RFC 3339 duration with regular expressions is not
2136
 
    # possible - there would have to be multiple places for the same
2137
 
    # values, like seconds.  The current code, while more esoteric, is
2138
 
    # cleaner without depending on a parsing library.  If Python had a
2139
 
    # built-in library for parsing we would use it, but we'd like to
2140
 
    # avoid excessive use of external libraries.
2141
 
    
2142
 
    # New type for defining tokens, syntax, and semantics all-in-one
2143
 
    Token = collections.namedtuple("Token",
2144
 
                                   ("regexp", # To match token; if
2145
 
                                              # "value" is not None,
2146
 
                                              # must have a "group"
2147
 
                                              # containing digits
2148
 
                                    "value",  # datetime.timedelta or
2149
 
                                              # None
2150
 
                                    "followers")) # Tokens valid after
2151
 
                                                  # this token
2152
 
    # RFC 3339 "duration" tokens, syntax, and semantics; taken from
2153
 
    # the "duration" ABNF definition in RFC 3339, Appendix A.
2154
 
    token_end = Token(re.compile(r"$"), None, frozenset())
2155
 
    token_second = Token(re.compile(r"(\d+)S"),
2156
 
                         datetime.timedelta(seconds=1),
2157
 
                         frozenset((token_end,)))
2158
 
    token_minute = Token(re.compile(r"(\d+)M"),
2159
 
                         datetime.timedelta(minutes=1),
2160
 
                         frozenset((token_second, token_end)))
2161
 
    token_hour = Token(re.compile(r"(\d+)H"),
2162
 
                       datetime.timedelta(hours=1),
2163
 
                       frozenset((token_minute, token_end)))
2164
 
    token_time = Token(re.compile(r"T"),
2165
 
                       None,
2166
 
                       frozenset((token_hour, token_minute,
2167
 
                                  token_second)))
2168
 
    token_day = Token(re.compile(r"(\d+)D"),
2169
 
                      datetime.timedelta(days=1),
2170
 
                      frozenset((token_time, token_end)))
2171
 
    token_month = Token(re.compile(r"(\d+)M"),
2172
 
                        datetime.timedelta(weeks=4),
2173
 
                        frozenset((token_day, token_end)))
2174
 
    token_year = Token(re.compile(r"(\d+)Y"),
2175
 
                       datetime.timedelta(weeks=52),
2176
 
                       frozenset((token_month, token_end)))
2177
 
    token_week = Token(re.compile(r"(\d+)W"),
2178
 
                       datetime.timedelta(weeks=1),
2179
 
                       frozenset((token_end,)))
2180
 
    token_duration = Token(re.compile(r"P"), None,
2181
 
                           frozenset((token_year, token_month,
2182
 
                                      token_day, token_time,
2183
 
                                      token_week))),
2184
 
    # Define starting values
2185
 
    value = datetime.timedelta() # Value so far
2186
 
    found_token = None
2187
 
    followers = frozenset(token_duration,) # Following valid tokens
2188
 
    s = duration                # String left to parse
2189
 
    # Loop until end token is found
2190
 
    while found_token is not token_end:
2191
 
        # Search for any currently valid tokens
2192
 
        for token in followers:
2193
 
            match = token.regexp.match(s)
2194
 
            if match is not None:
2195
 
                # Token found
2196
 
                if token.value is not None:
2197
 
                    # Value found, parse digits
2198
 
                    factor = int(match.group(1), 10)
2199
 
                    # Add to value so far
2200
 
                    value += factor * token.value
2201
 
                # Strip token from string
2202
 
                s = token.regexp.sub("", s, 1)
2203
 
                # Go to found token
2204
 
                found_token = token
2205
 
                # Set valid next tokens
2206
 
                followers = found_token.followers
2207
 
                break
2208
 
        else:
2209
 
            # No currently valid tokens were found
2210
 
            raise ValueError("Invalid RFC 3339 duration")
2211
 
    # End token found
2212
 
    return value
2213
 
 
2214
 
 
2215
2093
def string_to_delta(interval):
2216
2094
    """Parse a string and return a datetime.timedelta
2217
2095
    
2228
2106
    >>> string_to_delta('5m 30s')
2229
2107
    datetime.timedelta(0, 330)
2230
2108
    """
2231
 
    
2232
 
    try:
2233
 
        return rfc3339_duration_to_delta(interval)
2234
 
    except ValueError:
2235
 
        pass
2236
 
    
2237
2109
    timevalue = datetime.timedelta(0)
2238
2110
    for s in interval.split():
2239
2111
        try:
2302
2174
                        help="Run self-test")
2303
2175
    parser.add_argument("--debug", action="store_true",
2304
2176
                        help="Debug mode; run in foreground and log"
2305
 
                        " to terminal", default=None)
 
2177
                        " to terminal")
2306
2178
    parser.add_argument("--debuglevel", metavar="LEVEL",
2307
2179
                        help="Debug level for stdout output")
2308
2180
    parser.add_argument("--priority", help="GnuTLS"
2315
2187
                        " files")
2316
2188
    parser.add_argument("--no-dbus", action="store_false",
2317
2189
                        dest="use_dbus", help="Do not provide D-Bus"
2318
 
                        " system bus interface", default=None)
 
2190
                        " system bus interface")
2319
2191
    parser.add_argument("--no-ipv6", action="store_false",
2320
 
                        dest="use_ipv6", help="Do not use IPv6",
2321
 
                        default=None)
 
2192
                        dest="use_ipv6", help="Do not use IPv6")
2322
2193
    parser.add_argument("--no-restore", action="store_false",
2323
2194
                        dest="restore", help="Do not restore stored"
2324
 
                        " state", default=None)
 
2195
                        " state")
2325
2196
    parser.add_argument("--socket", type=int,
2326
2197
                        help="Specify a file descriptor to a network"
2327
2198
                        " socket to use instead of creating one")
2328
2199
    parser.add_argument("--statedir", metavar="DIR",
2329
2200
                        help="Directory to save/restore state in")
2330
2201
    parser.add_argument("--foreground", action="store_true",
2331
 
                        help="Run in foreground", default=None)
 
2202
                        help="Run in foreground")
2332
2203
    
2333
2204
    options = parser.parse_args()
2334
2205
    
2343
2214
                        "port": "",
2344
2215
                        "debug": "False",
2345
2216
                        "priority":
2346
 
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224",
 
2217
                        "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
2347
2218
                        "servicename": "Mandos",
2348
2219
                        "use_dbus": "True",
2349
2220
                        "use_ipv6": "True",
2393
2264
    for option in server_settings.keys():
2394
2265
        if type(server_settings[option]) is str:
2395
2266
            server_settings[option] = unicode(server_settings[option])
2396
 
    # Force all boolean options to be boolean
2397
 
    for option in ("debug", "use_dbus", "use_ipv6", "restore",
2398
 
                   "foreground"):
2399
 
        server_settings[option] = bool(server_settings[option])
2400
2267
    # Debug implies foreground
2401
2268
    if server_settings["debug"]:
2402
2269
        server_settings["foreground"] = True
2542
2409
    old_client_settings = {}
2543
2410
    clients_data = {}
2544
2411
    
2545
 
    # This is used to redirect stdout and stderr for checker processes
2546
 
    global wnull
2547
 
    wnull = open(os.devnull, "w") # A writable /dev/null
2548
 
    # Only used if server is running in foreground but not in debug
2549
 
    # mode
2550
 
    if debug or not foreground:
2551
 
        wnull.close()
2552
 
    
2553
2412
    # Get client data and settings from last running state.
2554
2413
    if server_settings["restore"]:
2555
2414
        try:
2571
2430
    
2572
2431
    with PGPEngine() as pgp:
2573
2432
        for client_name, client in clients_data.iteritems():
2574
 
            # Skip removed clients
2575
 
            if client_name not in client_settings:
2576
 
                continue
2577
 
            
2578
2433
            # Decide which value to use after restoring saved state.
2579
2434
            # We have three different values: Old config file,
2580
2435
            # new config file, and saved state.
2642
2497
    # Create all client objects
2643
2498
    for client_name, client in clients_data.iteritems():
2644
2499
        tcp_server.clients[client_name] = client_class(
2645
 
            name = client_name, settings = client,
2646
 
            server_settings = server_settings)
 
2500
            name = client_name, settings = client)
2647
2501
    
2648
2502
    if not tcp_server.clients:
2649
2503
        logger.warning("No clients defined")
2732
2586
        service.cleanup()
2733
2587
        
2734
2588
        multiprocessing.active_children()
2735
 
        wnull.close()
2736
2589
        if not (tcp_server.clients or client_settings):
2737
2590
            return
2738
2591
        
2750
2603
                # A list of attributes that can not be pickled
2751
2604
                # + secret.
2752
2605
                exclude = set(("bus", "changedstate", "secret",
2753
 
                               "checker", "server_settings"))
 
2606
                               "checker"))
2754
2607
                for name, typ in (inspect.getmembers
2755
2608
                                  (dbus.service.Object)):
2756
2609
                    exclude.add(name)