/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 server.py

  • Committer: Björn Påhlsson
  • Date: 2008-07-20 02:52:20 UTC
  • Revision ID: belorn@braxen-20080720025220-r5u0388uy9iu23h6
Added following support:
Pluginbased client handler
rewritten Mandos client
       Avahi instead of udp server discovery
       openpgp encrypted key support
Passprompt stand alone application for direct console input
Added logging for Mandos server

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
import gnutls.crypto
12
12
import gnutls.connection
13
13
import gnutls.errors
 
14
import gnutls.library.functions
 
15
import gnutls.library.constants
 
16
import gnutls.library.types
14
17
import ConfigParser
15
18
import sys
16
19
import re
23
26
import gobject
24
27
import avahi
25
28
from dbus.mainloop.glib import DBusGMainLoop
26
 
 
27
 
# This variable is used to optionally bind to a specified
28
 
# interface.
 
29
import ctypes
 
30
 
 
31
import logging
 
32
import logging.handlers
 
33
 
 
34
# logghandler.setFormatter(logging.Formatter('%(levelname)s %(message)s')
 
35
 
 
36
logger = logging.Logger('mandos')
 
37
logger.addHandler(logging.handlers.SysLogHandler(facility = logging.handlers.SysLogHandler.LOG_DAEMON))
 
38
 
 
39
# This variable is used to optionally bind to a specified interface.
 
40
# It is a global variable to fit in with the other variables from the
 
41
# Avahi server example code.
29
42
serviceInterface = avahi.IF_UNSPEC
30
 
# It is a global variable to fit in with the rest of the
31
 
# variables from the Avahi server example code:
 
43
# From the Avahi server example code:
32
44
serviceName = "Mandos"
33
45
serviceType = "_mandos._tcp" # http://www.dns-sd.org/ServiceTypes.html
34
46
servicePort = None                      # Not known at startup
44
56
class Client(object):
45
57
    """A representation of a client host served by this server.
46
58
    Attributes:
47
 
    password:  string
48
 
    fqdn:      string, FQDN (used by the checker)
 
59
    name:      string; from the config file, used in log messages
 
60
    fingerprint: string (40 or 32 hexadecimal digits); used to
 
61
                 uniquely identify the client
 
62
    secret:    bytestring; sent verbatim (over TLS) to client
 
63
    fqdn:      string (FQDN); available for use by the checker command
49
64
    created:   datetime.datetime()
50
65
    last_seen: datetime.datetime() or None if not yet seen
51
66
    timeout:   datetime.timedelta(); How long from last_seen until
52
67
                                     this client is invalid
53
68
    interval:  datetime.timedelta(); How often to start a new checker
54
 
    timeout_milliseconds: Used by gobject.timeout_add()
55
 
    interval_milliseconds: - '' -
56
69
    stop_hook: If set, called by stop() as stop_hook(self)
57
70
    checker:   subprocess.Popen(); a running checker process used
58
71
                                   to see if the client lives.
60
73
    checker_initiator_tag: a gobject event source tag, or None
61
74
    stop_initiator_tag:    - '' -
62
75
    checker_callback_tag:  - '' -
 
76
    checker_command: string; External command which is run to check if
 
77
                     client lives.  %()s expansions are done at
 
78
                     runtime with vars(self) as dict, so that for
 
79
                     instance %(name)s can be used in the command.
 
80
    Private attibutes:
 
81
    _timeout: Real variable for 'timeout'
 
82
    _interval: Real variable for 'interval'
 
83
    _timeout_milliseconds: Used by gobject.timeout_add()
 
84
    _interval_milliseconds: - '' -
63
85
    """
 
86
    def _set_timeout(self, timeout):
 
87
        "Setter function for 'timeout' attribute"
 
88
        self._timeout = timeout
 
89
        self._timeout_milliseconds = ((self.timeout.days
 
90
                                       * 24 * 60 * 60 * 1000)
 
91
                                      + (self.timeout.seconds * 1000)
 
92
                                      + (self.timeout.microseconds
 
93
                                         // 1000))
 
94
    timeout = property(lambda self: self._timeout,
 
95
                       _set_timeout)
 
96
    del _set_timeout
 
97
    def _set_interval(self, interval):
 
98
        "Setter function for 'interval' attribute"
 
99
        self._interval = interval
 
100
        self._interval_milliseconds = ((self.interval.days
 
101
                                        * 24 * 60 * 60 * 1000)
 
102
                                       + (self.interval.seconds
 
103
                                          * 1000)
 
104
                                       + (self.interval.microseconds
 
105
                                          // 1000))
 
106
    interval = property(lambda self: self._interval,
 
107
                        _set_interval)
 
108
    del _set_interval
64
109
    def __init__(self, name=None, options=None, stop_hook=None,
65
 
                 dn=None, password=None, passfile=None, fqdn=None,
66
 
                 timeout=None, interval=-1):
 
110
                 fingerprint=None, secret=None, secfile=None, fqdn=None,
 
111
                 timeout=None, interval=-1, checker=None):
67
112
        self.name = name
68
 
        self.dn = dn
69
 
        if password:
70
 
            self.password = password
71
 
        elif passfile:
72
 
            self.password = open(passfile).readall()
 
113
        # Uppercase and remove spaces from fingerprint
 
114
        # for later comparison purposes with return value of
 
115
        # the fingerprint() function
 
116
        self.fingerprint = fingerprint.upper().replace(u" ", u"")
 
117
        if secret:
 
118
            self.secret = secret.decode(u"base64")
 
119
        elif secfile:
 
120
            sf = open(secfile)
 
121
            self.secret = sf.read()
 
122
            sf.close()
73
123
        else:
74
 
            raise RuntimeError(u"No Password or Passfile for client %s"
 
124
            raise RuntimeError(u"No secret or secfile for client %s"
75
125
                               % self.name)
76
126
        self.fqdn = fqdn                # string
77
127
        self.created = datetime.datetime.now()
79
129
        if timeout is None:
80
130
            timeout = options.timeout
81
131
        self.timeout = timeout
82
 
        self.timeout_milliseconds = ((self.timeout.days
83
 
                                      * 24 * 60 * 60 * 1000)
84
 
                                     + (self.timeout.seconds * 1000)
85
 
                                     + (self.timeout.microseconds
86
 
                                        // 1000))
87
132
        if interval == -1:
88
133
            interval = options.interval
89
134
        else:
90
135
            interval = string_to_delta(interval)
91
136
        self.interval = interval
92
 
        self.interval_milliseconds = ((self.interval.days
93
 
                                       * 24 * 60 * 60 * 1000)
94
 
                                      + (self.interval.seconds * 1000)
95
 
                                      + (self.interval.microseconds
96
 
                                         // 1000))
97
137
        self.stop_hook = stop_hook
98
138
        self.checker = None
99
139
        self.checker_initiator_tag = None
100
140
        self.stop_initiator_tag = None
101
141
        self.checker_callback_tag = None
 
142
        self.check_command = checker
102
143
    def start(self):
103
144
        """Start this clients checker and timeout hooks"""
104
145
        # Schedule a new checker to be started an 'interval' from now,
105
146
        # and every interval from then on.
106
 
        self.checker_initiator_tag = gobject.\
107
 
                                     timeout_add(self.interval_milliseconds,
108
 
                                                 self.start_checker)
 
147
        self.checker_initiator_tag = gobject.timeout_add\
 
148
                                     (self._interval_milliseconds,
 
149
                                      self.start_checker)
109
150
        # Also start a new checker *right now*.
110
151
        self.start_checker()
111
152
        # Schedule a stop() when 'timeout' has passed
112
 
        self.stop_initiator_tag = gobject.\
113
 
                                     timeout_add(self.timeout_milliseconds,
114
 
                                                 self.stop)
 
153
        self.stop_initiator_tag = gobject.timeout_add\
 
154
                                  (self._timeout_milliseconds,
 
155
                                   self.stop)
115
156
    def stop(self):
116
157
        """Stop this client.
117
158
        The possibility that this client might be restarted is left
118
159
        open, but not currently used."""
119
 
        # print "Stopping client", self.name
120
 
        self.password = None
 
160
        logger.debug(u"Stopping client %s", self.name)
 
161
        self.secret = None
121
162
        if self.stop_initiator_tag:
122
163
            gobject.source_remove(self.stop_initiator_tag)
123
164
            self.stop_initiator_tag = None
145
186
        now = datetime.datetime.now()
146
187
        if os.WIFEXITED(condition) \
147
188
               and (os.WEXITSTATUS(condition) == 0):
148
 
            #print "Checker for %(name)s succeeded" % vars(self)
 
189
            logger.debug(u"Checker for %(name)s succeeded",
 
190
                         vars(self))
149
191
            self.last_seen = now
150
192
            gobject.source_remove(self.stop_initiator_tag)
151
 
            self.stop_initiator_tag = gobject.\
152
 
                                      timeout_add(self.timeout_milliseconds,
153
 
                                                  self.stop)
154
 
        #else:
155
 
        #    if not os.WIFEXITED(condition):
156
 
        #        print "Checker for %(name)s crashed?" % vars(self)
157
 
        #    else:
158
 
        #        print "Checker for %(name)s failed" % vars(self)
159
 
        self.checker = None
 
193
            self.stop_initiator_tag = gobject.timeout_add\
 
194
                                      (self._timeout_milliseconds,
 
195
                                       self.stop)
 
196
        if not os.WIFEXITED(condition):
 
197
            logger.warning(u"Checker for %(name)s crashed?",
 
198
                           vars(self))
 
199
        else:
 
200
            logger.debug(u"Checker for %(name)s failed",
 
201
                         vars(self))
 
202
            self.checker = None
160
203
        self.checker_callback_tag = None
161
204
    def start_checker(self):
162
205
        """Start a new checker subprocess if one is not running.
163
206
        If a checker already exists, leave it running and do
164
207
        nothing."""
165
208
        if self.checker is None:
166
 
            #print "Starting checker for", self.name
 
209
            logger.debug(u"Starting checker for %s",
 
210
                         self.name)
 
211
            try:
 
212
                command = self.check_command % self.fqdn
 
213
            except TypeError:
 
214
                escaped_attrs = dict((key, re.escape(str(val)))
 
215
                                     for key, val in
 
216
                                     vars(self).iteritems())
 
217
                try:
 
218
                    command = self.check_command % escaped_attrs
 
219
                except TypeError, error:
 
220
                    logger.critical(u'Could not format string "%s": %s',
 
221
                                    self.check_command, error)
 
222
                    return True # Try again later
167
223
            try:
168
224
                self.checker = subprocess.\
169
 
                               Popen("sleep 1; fping -q -- %s"
170
 
                                     % re.escape(self.fqdn),
 
225
                               Popen(command,
171
226
                                     stdout=subprocess.PIPE,
172
227
                                     close_fds=True, shell=True,
173
228
                                     cwd="/")
176
231
                                                            self.\
177
232
                                                            checker_callback)
178
233
            except subprocess.OSError, error:
179
 
                sys.stderr.write(u"Failed to start subprocess: %s\n"
180
 
                                 % error)
 
234
                logger.error(u"Failed to start subprocess: %s",
 
235
                             error)
181
236
        # Re-run this periodically if run by gobject.timeout_add
182
237
        return True
183
238
    def stop_checker(self):
200
255
            return now < (self.last_seen + self.timeout)
201
256
 
202
257
 
 
258
def peer_certificate(session):
 
259
    # If not an OpenPGP certificate...
 
260
    if gnutls.library.functions.gnutls_certificate_type_get\
 
261
            (session._c_object) \
 
262
           != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
 
263
        # ...do the normal thing
 
264
        return session.peer_certificate
 
265
    list_size = ctypes.c_uint()
 
266
    cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
 
267
        (session._c_object, ctypes.byref(list_size))
 
268
    if list_size.value == 0:
 
269
        return None
 
270
    cert = cert_list[0]
 
271
    return ctypes.string_at(cert.data, cert.size)
 
272
 
 
273
 
 
274
def fingerprint(openpgp):
 
275
    # New empty GnuTLS certificate
 
276
    crt = gnutls.library.types.gnutls_openpgp_crt_t()
 
277
    gnutls.library.functions.gnutls_openpgp_crt_init\
 
278
        (ctypes.byref(crt))
 
279
    # New GnuTLS "datum" with the OpenPGP public key
 
280
    datum = gnutls.library.types.gnutls_datum_t\
 
281
        (ctypes.cast(ctypes.c_char_p(openpgp),
 
282
                     ctypes.POINTER(ctypes.c_ubyte)),
 
283
         ctypes.c_uint(len(openpgp)))
 
284
    # Import the OpenPGP public key into the certificate
 
285
    ret = gnutls.library.functions.gnutls_openpgp_crt_import\
 
286
        (crt,
 
287
         ctypes.byref(datum),
 
288
         gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
 
289
    # New buffer for the fingerprint
 
290
    buffer = ctypes.create_string_buffer(20)
 
291
    buffer_length = ctypes.c_size_t()
 
292
    # Get the fingerprint from the certificate into the buffer
 
293
    gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
 
294
        (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
 
295
    # Deinit the certificate
 
296
    gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
 
297
    # Convert the buffer to a Python bytestring
 
298
    fpr = ctypes.string_at(buffer, buffer_length.value)
 
299
    # Convert the bytestring to hexadecimal notation
 
300
    hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
 
301
    return hex_fpr
 
302
 
 
303
 
203
304
class tcp_handler(SocketServer.BaseRequestHandler, object):
204
305
    """A TCP request handler class.
205
306
    Instantiated by IPv6_TCPServer for each request to handle it.
206
307
    Note: This will run in its own forked process."""
 
308
    
207
309
    def handle(self):
208
 
        #print u"TCP request came"
209
 
        #print u"Request:", self.request
210
 
        #print u"Client Address:", self.client_address
211
 
        #print u"Server:", self.server
212
 
        session = gnutls.connection.ServerSession(self.request,
213
 
                                                  self.server\
214
 
                                                  .credentials)
 
310
        logger.debug(u"TCP connection from: %s",
 
311
                     unicode(self.client_address))
 
312
        session = gnutls.connection.ClientSession(self.request,
 
313
                                                  gnutls.connection.\
 
314
                                                  X509Credentials())
 
315
        
 
316
        #priority = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
 
317
        #                "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
 
318
        #                "+DHE-DSS"))
 
319
        priority = "SECURE256"
 
320
        
 
321
        gnutls.library.functions.gnutls_priority_set_direct\
 
322
            (session._c_object, priority, None);
 
323
        
215
324
        try:
216
325
            session.handshake()
217
326
        except gnutls.errors.GNUTLSError, error:
218
 
            #sys.stderr.write(u"Handshake failed: %s\n" % error)
 
327
            logger.debug(u"Handshake failed: %s", error)
219
328
            # Do not run session.bye() here: the session is not
220
329
            # established.  Just abandon the request.
221
330
            return
222
 
        #if session.peer_certificate:
223
 
        #    print "DN:", session.peer_certificate.subject
224
331
        try:
225
 
            session.verify_peer()
226
 
        except gnutls.errors.CertificateError, error:
227
 
            #sys.stderr.write(u"Verify failed: %s\n" % error)
 
332
            fpr = fingerprint(peer_certificate(session))
 
333
        except (TypeError, gnutls.errors.GNUTLSError), error:
 
334
            logger.debug(u"Bad certificate: %s", error)
228
335
            session.bye()
229
336
            return
 
337
        logger.debug(u"Fingerprint: %s", fpr)
230
338
        client = None
231
339
        for c in clients:
232
 
            if c.dn == session.peer_certificate.subject:
 
340
            if c.fingerprint == fpr:
233
341
                client = c
234
342
                break
235
343
        # Have to check if client.still_valid(), since it is possible
236
344
        # that the client timed out while establishing the GnuTLS
237
345
        # session.
238
 
        if client and client.still_valid():
239
 
            session.send(client.password)
240
 
        else:
241
 
            #if client:
242
 
            #    sys.stderr.write(u"Client %(name)s is invalid\n"
243
 
            #                     % vars(client))
244
 
            #else:
245
 
            #    sys.stderr.write(u"Client not found for DN: %s\n"
246
 
            #                     % session.peer_certificate.subject)
247
 
            #session.send("gazonk")
248
 
            pass
 
346
        if (not client) or (not client.still_valid()):
 
347
            if client:
 
348
                logger.debug(u"Client %(name)s is invalid",
 
349
                             vars(client))
 
350
            else:
 
351
                logger.debug(u"Client not found for fingerprint: %s",
 
352
                             fpr)
 
353
            session.bye()
 
354
            return
 
355
        sent_size = 0
 
356
        while sent_size < len(client.secret):
 
357
            sent = session.send(client.secret[sent_size:])
 
358
            logger.debug(u"Sent: %d, remaining: %d",
 
359
                         sent, len(client.secret)
 
360
                         - (sent_size + sent))
 
361
            sent_size += sent
249
362
        session.bye()
250
363
 
251
364
 
254
367
    Attributes:
255
368
        options:        Command line options
256
369
        clients:        Set() of Client objects
257
 
        credentials:    GnuTLS X.509 credentials
258
370
    """
259
371
    address_family = socket.AF_INET6
260
372
    def __init__(self, *args, **kwargs):
264
376
        if "clients" in kwargs:
265
377
            self.clients = kwargs["clients"]
266
378
            del kwargs["clients"]
267
 
        if "credentials" in kwargs:
268
 
            self.credentials = kwargs["credentials"]
269
 
            del kwargs["credentials"]
270
379
        return super(type(self), self).__init__(*args, **kwargs)
271
380
    def server_bind(self):
272
381
        """This overrides the normal server_bind() function
282
391
                                       self.options.interface)
283
392
            except socket.error, error:
284
393
                if error[0] == errno.EPERM:
285
 
                    sys.stderr.write(u"Warning: No permission to bind to interface %s\n"
286
 
                                     % self.options.interface)
 
394
                    logger.warning(u"No permission to"
 
395
                                   u" bind to interface %s",
 
396
                                   self.options.interface)
287
397
                else:
288
398
                    raise error
289
399
        # Only bind(2) the socket if we really need to.
343
453
                avahi.DBUS_INTERFACE_ENTRY_GROUP)
344
454
        group.connect_to_signal('StateChanged',
345
455
                                entry_group_state_changed)
346
 
    
347
 
    # print "Adding service '%s' of type '%s' ..." % (serviceName,
348
 
    #                                                 serviceType)
 
456
    logger.debug(u"Adding service '%s' of type '%s' ...",
 
457
                 serviceName, serviceType)
349
458
    
350
459
    group.AddService(
351
460
            serviceInterface,           # interface
369
478
def server_state_changed(state):
370
479
    """From the Avahi server example code"""
371
480
    if state == avahi.SERVER_COLLISION:
372
 
        print "WARNING: Server name collision"
 
481
        logger.warning(u"Server name collision")
373
482
        remove_service()
374
483
    elif state == avahi.SERVER_RUNNING:
375
484
        add_service()
379
488
    """From the Avahi server example code"""
380
489
    global serviceName, server, rename_count
381
490
    
382
 
    # print "state change: %i" % state
 
491
    logger.debug(u"state change: %i", state)
383
492
    
384
493
    if state == avahi.ENTRY_GROUP_ESTABLISHED:
385
 
        pass
386
 
        # print "Service established."
 
494
        logger.debug(u"Service established.")
387
495
    elif state == avahi.ENTRY_GROUP_COLLISION:
388
496
        
389
497
        rename_count = rename_count - 1
390
498
        if rename_count > 0:
391
499
            name = server.GetAlternativeServiceName(name)
392
 
            print "WARNING: Service name collision, changing name to '%s' ..." % name
 
500
            logger.warning(u"Service name collision, "
 
501
                           u"changing name to '%s' ...", name)
393
502
            remove_service()
394
503
            add_service()
395
504
            
396
505
        else:
397
 
            print "ERROR: No suitable service name found after %i retries, exiting." % n_rename
 
506
            logger.error(u"No suitable service name found "
 
507
                         u"after %i retries, exiting.",
 
508
                         n_rename)
398
509
            main_loop.quit()
399
510
    elif state == avahi.ENTRY_GROUP_FAILURE:
400
 
        print "Error in group state changed", error
 
511
        logger.error(u"Error in group state changed %s",
 
512
                     unicode(error))
401
513
        main_loop.quit()
402
514
        return
403
515
 
405
517
def if_nametoindex(interface):
406
518
    """Call the C function if_nametoindex()"""
407
519
    try:
408
 
        if "ctypes" not in sys.modules:
409
 
            import ctypes
410
520
        libc = ctypes.cdll.LoadLibrary("libc.so.6")
411
521
        return libc.if_nametoindex(interface)
412
 
    except (ImportError, OSError, AttributeError):
 
522
    except (OSError, AttributeError):
413
523
        if "struct" not in sys.modules:
414
524
            import struct
415
525
        if "fcntl" not in sys.modules:
450
560
                      help="How often to check that a client is up")
451
561
    parser.add_option("--check", action="store_true", default=False,
452
562
                      help="Run self-test")
 
563
    parser.add_option("--debug", action="store_true", default=False,
 
564
                      help="Debug mode")
453
565
    (options, args) = parser.parse_args()
454
566
    
455
567
    if options.check:
467
579
    except ValueError:
468
580
        parser.error("option --interval: Unparseable time")
469
581
    
470
 
    cert = gnutls.crypto.X509Certificate(open(options.cert).read())
471
 
    key = gnutls.crypto.X509PrivateKey(open(options.key).read())
472
 
    ca = gnutls.crypto.X509Certificate(open(options.ca).read())
473
 
    crl = gnutls.crypto.X509CRL(open(options.crl).read())
474
 
    cred = gnutls.connection.X509Credentials(cert, key, [ca], [crl])
475
 
    
476
582
    # Parse config file
477
 
    defaults = {}
 
583
    defaults = { "checker": "sleep 1; fping -q -- %%(fqdn)s" }
478
584
    client_config = ConfigParser.SafeConfigParser(defaults)
479
585
    #client_config.readfp(open("secrets.conf"), "secrets.conf")
480
586
    client_config.read("mandos-clients.conf")
488
594
            avahi.DBUS_INTERFACE_SERVER )
489
595
    # End of Avahi example code
490
596
    
 
597
    debug = options.debug
 
598
    
491
599
    clients = Set()
492
600
    def remove_from_clients(client):
493
601
        clients.remove(client)
494
602
        if not clients:
495
 
            print "No clients left, exiting"
 
603
            logger.debug(u"No clients left, exiting")
496
604
            main_loop.quit()
497
605
    
498
606
    clients.update(Set(Client(name=section, options=options,
506
614
    tcp_server = IPv6_TCPServer((None, options.port),
507
615
                                tcp_handler,
508
616
                                options=options,
509
 
                                clients=clients,
510
 
                                credentials=cred)
 
617
                                clients=clients)
511
618
    # Find out what random port we got
512
619
    servicePort = tcp_server.socket.getsockname()[1]
513
 
    #sys.stderr.write("Now listening on port %d\n" % servicePort)
 
620
    logger.debug(u"Now listening on port %d", servicePort)
514
621
    
515
622
    if options.interface is not None:
516
623
        serviceInterface = if_nametoindex(options.interface)