diff options
| author | Adrian Likins <alikins@redhat.com> | 2007-09-25 11:50:28 -0400 |
|---|---|---|
| committer | Adrian Likins <alikins@redhat.com> | 2007-09-25 11:50:28 -0400 |
| commit | fb7df2a5b7834ead29effb5c60bdc33558546631 (patch) | |
| tree | 6c70ccfb037ab621cc69085ba8d5b540753d81dd /func | |
| parent | 9c72cbd826528bb64267ba2184ae16099343c7ab (diff) | |
| parent | 47100aa2f165b47175af1e1aef736c5769a83169 (diff) | |
| download | func-fb7df2a5b7834ead29effb5c60bdc33558546631.tar.gz func-fb7df2a5b7834ead29effb5c60bdc33558546631.tar.xz func-fb7df2a5b7834ead29effb5c60bdc33558546631.zip | |
Merge branch 'master' of ssh://git.fedoraproject.org/git/hosted/func
Diffstat (limited to 'func')
| -rw-r--r-- | func/CommonErrors.py | 62 | ||||
| -rw-r--r-- | func/SSLCommon.py | 122 | ||||
| -rw-r--r-- | func/SSLConnection.py | 161 | ||||
| -rwxr-xr-x | func/certmaster.py | 171 |
4 files changed, 516 insertions, 0 deletions
diff --git a/func/CommonErrors.py b/func/CommonErrors.py new file mode 100644 index 0000000..13ef505 --- /dev/null +++ b/func/CommonErrors.py @@ -0,0 +1,62 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc. + +import socket + + +def canIgnoreSSLError(e): + """ + Identify common network errors that mean we cannot connect to the server + """ + + # This is a bit complicated by the fact that different versions of + # M2Crypto & OpenSSL seem to return different error codes for the + # same type of error + s = "%s" % e + if e[0] == 104: # Connection refused + return True + elif e[0] == 111: # Connection reset by peer + return True + elif e[0] == 61: # Connection refused + return True + elif e[0] == 54: # Connection reset by peer + return True + elif s == "no certificate returned": + return True + elif s == "wrong version number": + return True + elif s == "unexpected eof": + return True + + return False + + +def canIgnoreSocketError(e): + """ + Identify common network errors that mean we cannot connect to the server + """ + + try: + if e[0] == 111: # Connection refused + return True + elif e[0] == 104: # Connection reset by peer + return True + elif e[0] == 61: # Connection refused + return True + except IndexError: + return True + + return False diff --git a/func/SSLCommon.py b/func/SSLCommon.py new file mode 100644 index 0000000..bdfd719 --- /dev/null +++ b/func/SSLCommon.py @@ -0,0 +1,122 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc. + +import os, sys +import CommonErrors +from OpenSSL import SSL +import SSLConnection +import httplib +import socket +import SocketServer + +def our_verify(connection, x509, errNum, errDepth, preverifyOK): + # print "Verify: errNum = %s, errDepth = %s, preverifyOK = %s" % (errNum, errDepth, preverifyOK) + + # preverifyOK should tell us whether or not the client's certificate + # correctly authenticates against the CA chain + return preverifyOK + + +def CreateSSLContext(pkey, cert, ca_cert): + for f in pkey, cert, ca_cert: + if f and not os.access(f, os.R_OK): + print "%s does not exist or is not readable." % f + os._exit(1) + + ctx = SSL.Context(SSL.SSLv3_METHOD) # SSLv3 only + ctx.use_certificate_file(cert) + ctx.use_privatekey_file(pkey) + ctx.load_client_ca(ca_cert) + ctx.load_verify_locations(ca_cert) + verify = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT + ctx.set_verify(verify, our_verify) + ctx.set_verify_depth(10) + ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_TLSv1) + return ctx + + + +class BaseServer(SocketServer.TCPServer): + allow_reuse_address = 1 + + def __init__(self, server_addr, req_handler): + self._quit = False + self.allow_reuse_address = 1 + SocketServer.TCPServer.__init__(self, server_addr, req_handler) + + def stop(self): + self._quit = True + + def serve_forever(self): + while not self._quit: + self.handle_request() + self.server_close() + + +class BaseSSLServer(BaseServer): + """ SSL-enabled variant """ + + def __init__(self, server_address, req_handler, pkey, cert, ca_cert, timeout=None): + self._timeout = timeout + self.ssl_ctx = CreateSSLContext(pkey, cert, ca_cert) + + BaseServer.__init__(self, server_address, req_handler) + + sock = socket.socket(self.address_family, self.socket_type) + con = SSL.Connection(self.ssl_ctx, sock) + self.socket = SSLConnection.SSLConnection(con) + if sys.version_info[:3] >= (2, 3, 0): + self.socket.settimeout(self._timeout) + self.server_bind() + self.server_activate() + + host, port = self.socket.getsockname()[:2] + self.server_name = socket.getfqdn(host) + self.server_port = port + + +class HTTPSConnection(httplib.HTTPConnection): + "This class allows communication via SSL." + + response_class = httplib.HTTPResponse + + def __init__(self, host, port=None, ssl_context=None, strict=None, timeout=None): + httplib.HTTPConnection.__init__(self, host, port, strict) + self.ssl_ctx = ssl_context + self._timeout = timeout + + def connect(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + con = SSL.Connection(self.ssl_ctx, sock) + self.sock = SSLConnection.SSLConnection(con) + if sys.version_info[:3] >= (2, 3, 0): + self.sock.settimeout(self._timeout) + self.sock.connect((self.host, self.port)) + + +class HTTPS(httplib.HTTP): + """Compatibility with 1.5 httplib interface + + Python 1.5.2 did not have an HTTPS class, but it defined an + interface for sending http requests that is also useful for + https. + """ + + _connection_class = HTTPSConnection + + def __init__(self, host='', port=None, ssl_context=None, strict=None, timeout=None): + self._setup(self._connection_class(host, port, ssl_context, strict, timeout)) + diff --git a/func/SSLConnection.py b/func/SSLConnection.py new file mode 100644 index 0000000..47529b4 --- /dev/null +++ b/func/SSLConnection.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# +# Higher-level SSL objects used by rpclib +# +# Copyright (c) 2002 Red Hat, Inc. +# +# Author: Mihai Ibanescu <misa@redhat.com> +# Modifications by Dan Williams <dcbw@redhat.com> + + +from OpenSSL import SSL, crypto +import os, string, time, socket, select + + +class SSLConnection: + """ + This whole class exists just to filter out a parameter + passed in to the shutdown() method in SimpleXMLRPC.doPOST() + """ + + DEFAULT_TIMEOUT = 20 + + def __init__(self, conn): + """ + Connection is not yet a new-style class, + so I'm making a proxy instead of subclassing. + """ + self.__dict__["conn"] = conn + self.__dict__["close_refcount"] = 0 + self.__dict__["closed"] = False + self.__dict__["timeout"] = self.DEFAULT_TIMEOUT + + def __del__(self): + self.__dict__["conn"].close() + + def __getattr__(self,name): + return getattr(self.__dict__["conn"], name) + + def __setattr__(self,name, value): + setattr(self.__dict__["conn"], name, value) + + def settimeout(self, timeout): + if timeout == None: + self.__dict__["timeout"] = self.DEFAULT_TIMEOUT + else: + self.__dict__["timeout"] = timeout + self.__dict__["conn"].settimeout(timeout) + + def shutdown(self, how=1): + """ + SimpleXMLRpcServer.doPOST calls shutdown(1), + and Connection.shutdown() doesn't take + an argument. So we just discard the argument. + """ + self.__dict__["conn"].shutdown() + + def accept(self): + """ + This is the other part of the shutdown() workaround. + Since servers create new sockets, we have to infect + them with our magic. :) + """ + c, a = self.__dict__["conn"].accept() + return (SSLConnection(c), a) + + def makefile(self, mode, bufsize): + """ + We need to use socket._fileobject Because SSL.Connection + doesn't have a 'dup'. Not exactly sure WHY this is, but + this is backed up by comments in socket.py and SSL/connection.c + + Since httplib.HTTPSResponse/HTTPConnection depend on the + socket being duplicated when they close it, we refcount the + socket object and don't actually close until its count is 0. + """ + self.__dict__["close_refcount"] = self.__dict__["close_refcount"] + 1 + return PlgFileObject(self, mode, bufsize) + + def close(self): + if self.__dict__["closed"]: + return + self.__dict__["close_refcount"] = self.__dict__["close_refcount"] - 1 + if self.__dict__["close_refcount"] == 0: + self.shutdown() + self.__dict__["conn"].close() + self.__dict__["closed"] = True + + def sendall(self, data, flags=0): + """ + - Use select() to simulate a socket timeout without setting the socket + to non-blocking mode. + - Don't use pyOpenSSL's sendall() either, since it just loops on WantRead + or WantWrite, consuming 100% CPU, and never times out. + """ + timeout = self.__dict__["timeout"] + con = self.__dict__["conn"] + (read, write, excpt) = select.select([], [con], [], timeout) + if not con in write: + raise socket.timeout((110, "Operation timed out.")) + + starttime = time.time() + origlen = len(data) + sent = -1 + while len(data): + curtime = time.time() + if curtime - starttime > timeout: + raise socket.timeout((110, "Operation timed out.")) + + try: + sent = con.send(data, flags) + except SSL.SysCallError, e: + if e[0] == 32: # Broken Pipe + self.close() + sent = 0 + else: + raise socket.error(e) + except (SSL.WantWriteError, SSL.WantReadError): + time.sleep(0.2) + continue + + data = data[sent:] + return origlen - len(data) + + def recv(self, bufsize, flags=0): + """ + Use select() to simulate a socket timeout without setting the socket + to non-blocking mode + """ + timeout = self.__dict__["timeout"] + con = self.__dict__["conn"] + (read, write, excpt) = select.select([con], [], [], timeout) + if not con in read: + raise socket.timeout((110, "Operation timed out.")) + + starttime = time.time() + while True: + curtime = time.time() + if curtime - starttime > timeout: + raise socket.timeout((110, "Operation timed out.")) + + try: + return con.recv(bufsize, flags) + except SSL.ZeroReturnError: + return None + except SSL.WantReadError: + time.sleep(0.2) + return None + +class PlgFileObject(socket._fileobject): + def close(self): + """ + socket._fileobject doesn't actually _close_ the socket, + which we want it to do, so we have to override. + """ + try: + if self._sock: + self.flush() + self._sock.close() + finally: + self._sock = None + diff --git a/func/certmaster.py b/func/certmaster.py new file mode 100755 index 0000000..02c8013 --- /dev/null +++ b/func/certmaster.py @@ -0,0 +1,171 @@ +#!/usr/bin/python + +""" +cert master listener + +Copyright 2007, Red Hat, Inc +see AUTHORS + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + +# standard modules +import SimpleXMLRPCServer +import string +import socket +import sys +import os +import os.path +import traceback +from OpenSSL import crypto +import sha + +#from func.server import codes +import func +import func.certs + + +class SimpleConfigFile(object): + """simple config file object: + reads in key=value pairs from a file and stores each as an attribute""" + + def __init__(self, filename): + self.fn = filename + fo = open(filename, 'r') + for line in fo.readlines(): + if line.startswith('#'): continue + if line.strip() == '': continue + (key, val) = line.split('=') + key = key.strip().lower() + val = val.strip() + setattr(self, key, val) + fo.close() + + +class CertMaster(object): + def __init__(self, conf_file): + self.cfg = SimpleConfigFile(conf_file) + self.listen_addr = 'localhost' + self.listen_port = '51235' + self.cadir = '/etc/pki/func/ca' + self.certroot = '/etc/pki/func/ca/certs' + self.csrroot = '/etc/pki/func/ca/csrs' + self.autosign = True + for attr in ['listen_addr', 'listen_port', 'cadir', 'certroot', + 'csrroot']: + if hasattr(self.cfg, attr): + setattr(self, attr, getattr(self.cfg, attr)) + if hasattr(self.cfg, 'autosign'): + if getattr(self.cfg, 'autosign').lower() in ['yes', 'true', 1, 'on']: + self.autosign = True + elif getattr(self.cfg, 'autosign').lower() in ['no', 'false', 0, 'off']: + self.autosign = False + # open up the cakey and cacert so we have them available + ca_key_file = '%s/funcmaster.key' % self.cadir + ca_cert_file = '%s/funcmaster.crt' % self.cadir + self.cakey = func.certs.retrieve_key_from_file(ca_key_file) + self.cacert = func.certs.retrieve_cert_from_file(ca_cert_file) + + for dirpath in [self.cadir, self.certroot, self.csrroot]: + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + # setup handlers + self.handlers = { + 'wait_for_cert': self.wait_for_cert, + } + + def _dispatch(self, method, params): + if method == 'trait_names' or method == '_getAttributeNames': + return self.handlers.keys() + + if method in self.handlers.keys(): + return self.handlers[method](*params) + else: + pass + #raise codes.InvalidMethodException + + + def wait_for_cert(self, csrbuf): + """ + takes csr as a string + returns True, caller_cert, ca_cert + returns False, '', '' + """ + + try: + csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csrbuf) + except crypto.Error, e: + #XXX need to raise a fault here and document it - but false is just as good + return False, '', '' + + requesting_host = csrreq.get_subject().CN + certfile = '%s/%s.pem' % (self.certroot, requesting_host) + csrfile = '%s/%s.csr' % (self.csrroot, requesting_host) + + # check for old csr on disk + # if we have it - compare the two - if they are not the same - raise a fault + if os.path.exists(csrfile): + oldfo = open(csrfile) + oldcsrbuf = oldfo.read() + oldsha = sha.new() + oldsha.update(oldcsrbuf) + olddig = oldsha.hexdigest() + newsha = sha.new() + newsha.update(csrbuf) + newdig = newsha.hexdigest() + if not newdig == olddig: + # XXX raise a proper fault + return False, '', '' + + # look for a cert: + # if we have it, then return True, etc, etc + if os.path.exists(certfile): + slavecert = crypto.load_certificate(crypto.FILETYPE_PEM, certfile) + + cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert) + cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert) + return True, cert_buf, cacert_buf + + # if we don't have a cert then: + # if we're autosign then sign it, write out the cert and return True, etc, etc + # else write out the csr + + if self.autosign: + slavecert = func.certs.create_slave_certificate(csrreq, + self.cakey, self.cacert, self.cadir) + + destfo = open(certfile, 'w') + destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert)) + destfo.close() + del destfo + cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert) + cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert) + return True, cert_buf, cacert_buf + + else: + # write the csr out to a file to be dealt with by the admin + destfo = open(csrfile, 'w') + destfo.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csrreq)) + destfo.close() + del destfo + return False, '', '' + + return False, '', '' + + +def serve(xmlrpcinstance): + + """ + Code for starting the XMLRPC service. + """ + + server =FuncXMLRPCServer((xmlrpcinstance.listen_addr, xmlrpcinstance.list_port)) + server.logRequests = 0 # don't print stuff to console + server.register_instance(xmlrpcinstance) + server.serve_forever() |
