# $Id$ # Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. """HTTPS utilities, both client and server. At the moment this only knows how to use the PEM certs in my subversion repository; generalizing it would not be hard, but the more general version should use SQL anyway. """ import httplib, BaseHTTPServer, tlslite.api, glob, traceback, urlparse, socket import rpki.x509, rpki.exceptions, rpki.log # This should be wrapped somewhere in rpki.x509 eventually import POW # Do not set this to True for production use! disable_tls_certificate_validation_exceptions = False # Chatter about TLS certificates debug_tls_certs = False rpki_content_type = "application/x-rpki" def tlslite_certChain(x509): """Utility function to construct tlslite certChains.""" if isinstance(x509, rpki.x509.X509): return tlslite.api.X509CertChain([x509.get_tlslite()]) else: return tlslite.api.X509CertChain([x.get_tlslite() for x in x509]) class Checker(tlslite.api.Checker): """Derived class to handle X.509 client certificate checking.""" ## @var refuse_tls_ca_certs # Raise an exception upon receiving CA certificates via TLS rather # than just quietly ignoring them. refuse_tls_ca_certs = False ## @var pem_dump_tls_certs # Vile debugging hack pem_dump_tls_certs = False def __init__(self, trust_anchor = None, dynamic_x509store = None): """Initialize our modified certificate checker.""" self.dynamic_x509store = dynamic_x509store if dynamic_x509store is not None: return self.x509store = POW.X509Store() trust_anchor = rpki.x509.X509.normalize_chain(trust_anchor) assert trust_anchor for x in trust_anchor: if debug_tls_certs: rpki.log.debug("HTTPS trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) self.x509store.addTrust(x.get_POW()) if self.pem_dump_tls_certs: print x.get_PEM() def x509store_thunk(self): if self.dynamic_x509store is not None: return self.dynamic_x509store() else: return self.x509store def __call__(self, tlsConnection): """POW/OpenSSL-based certificate checker. Given our BPKI model, we're only interested in the TLS EE certificates. """ if tlsConnection._client: chain = tlsConnection.session.serverCertChain peer = "server" else: chain = tlsConnection.session.clientCertChain peer = "client" chain = [rpki.x509.X509(tlslite = chain.x509List[i]) for i in range(chain.getNumCerts())] ee = None for x in chain: if debug_tls_certs: rpki.log.debug("Received %s TLS %s cert issuer %s [%s] subject %s [%s]" % (peer, "CA" if x.is_CA() else "EE", x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) if self.pem_dump_tls_certs: print x.get_PEM() if x.is_CA(): if self.refuse_tls_ca_certs: raise rpki.exceptions.ReceivedTLSCACert continue if ee is not None: raise rpki.exceptions.MultipleTLSEECert, chain ee = x result = self.x509store_thunk().verifyDetailed(ee.get_POW()) if not result[0]: rpki.log.debug("TLS certificate validation result %s" % repr(result)) if disable_tls_certificate_validation_exceptions: rpki.log.warn("DANGER WILL ROBINSON! IGNORING TLS VALIDATION FAILURE!") else: raise rpki.exceptions.TLSValidationError class httpsClient(tlslite.api.HTTPTLSConnection): """Derived class to let us replace the default Checker.""" def __init__(self, host, port = None, client_cert = None, client_key = None, server_ta = None, settings = None): """Create a new httpsClient.""" tlslite.api.HTTPTLSConnection.__init__( self, host = host, port = port, settings = settings, certChain = client_cert, privateKey = client_key) self.checker = Checker(trust_anchor = server_ta) def client(msg, client_key, client_cert, server_ta, url, timeout = 300): """Open client HTTPS connection, send a message, wait for response. This function wraps most of what one needs to do to send a message over HTTPS and get a response. The certificate checking isn't quite up to snuff; it's better than with the other packages I've found, but doesn't appear to handle subjectAltName extensions (sigh). """ u = urlparse.urlparse(url) assert u.scheme in ("", "https") and \ u.username is None and \ u.password is None and \ u.params == "" and \ u.query == "" and \ u.fragment == "" rpki.log.debug("Contacting %s" % url) if debug_tls_certs: for cert in (client_cert,) if isinstance(client_cert, rpki.x509.X509) else client_cert: rpki.log.debug("Sending client TLS cert issuer %s subject %s" % (cert.getIssuer(), cert.getSubject())) # We could add a "settings = foo" argument to the following call to # pass in a tlslite.HandshakeSettings object that would let us # insist on, eg, particular SSL/TLS versions. httpc = httpsClient(host = u.hostname or "localhost", port = u.port or 443, client_key = client_key.get_tlslite(), client_cert = tlslite_certChain(client_cert), server_ta = server_ta) httpc.connect() httpc.sock.settimeout(timeout) httpc.request("POST", u.path, msg, {"Content-Type" : rpki_content_type}) response = httpc.getresponse() if response.status == httplib.OK: return response.read() else: r = response.read() raise rpki.exceptions.HTTPRequestFailed, \ "HTTP request failed with status %s, response %s" % (response.status, r) class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """Derived type to supply POST handler and override logging.""" rpki_handlers = None # Subclass must bind def rpki_find_handler(self): """Helper method to search self.rpki_handlers.""" for s,h in self.rpki_handlers: if self.path.startswith(s): return h return None def do_POST(self): """POST handler.""" try: handler = self.rpki_find_handler() if self.headers["Content-Type"] != rpki_content_type: rcode, rtext = 415, "Received Content-Type %s, expected %s" \ % (self.headers["Content-Type"], rpki_content_type) elif handler is None: rcode, rtext = 404, "No handler found for URL " + self.path else: rcode, rtext = handler(query = self.rfile.read(int(self.headers["Content-Length"])), path = self.path) except Exception, edata: rpki.log.error(traceback.format_exc()) rcode, rtext = 500, "Unhandled exception %s" % edata self.send_response(rcode) self.send_header("Content-Type", rpki_content_type) self.end_headers() self.wfile.write(rtext) def log_message(self, format, *args): """Redirect HTTP server logging into our own logging system.""" if args: rpki.log.info(format % args) else: rpki.log.info(format) class httpsServer(tlslite.api.TLSSocketServerMixIn, BaseHTTPServer.HTTPServer): """Derived type to handle TLS aspects of HTTPS.""" rpki_sessionCache = None rpki_server_key = None rpki_server_cert = None rpki_checker = None def handshake(self, tlsConnection): """TLS handshake handler.""" assert self.rpki_server_cert is not None assert self.rpki_server_key is not None assert self.rpki_sessionCache is not None try: # # We could add a "settings = foo" argument to the following call # to pass in a tlslite.HandshakeSettings object that would let # us insist on, eg, particular SSL/TLS versions. # tlsConnection.handshakeServer(certChain = self.rpki_server_cert, privateKey = self.rpki_server_key, sessionCache = self.rpki_sessionCache, checker = self.rpki_checker, reqCert = True) tlsConnection.ignoreAbruptClose = True return True except (tlslite.api.TLSError, rpki.exceptions.TLSValidationError), error: rpki.log.warn("TLS handshake failure: " + str(error)) return False def server(handlers, server_key, server_cert, port = 4433, host = "", client_ta = None, dynamic_x509store = None): """Run an HTTPS server and wait (forever) for connections.""" if not isinstance(handlers, (tuple, list)): handlers = (("/", handlers),) class boundRequestHandler(requestHandler): rpki_handlers = handlers httpd = httpsServer((host, port), boundRequestHandler) httpd.rpki_server_key = server_key.get_tlslite() httpd.rpki_server_cert = tlslite_certChain(server_cert) httpd.rpki_sessionCache = tlslite.api.SessionCache() httpd.rpki_checker = Checker(trust_anchor = client_ta, dynamic_x509store = dynamic_x509store) httpd.serve_forever()