diff options
Diffstat (limited to 'rpkid.stable/rpki/https.py')
-rw-r--r-- | rpkid.stable/rpki/https.py | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/rpkid.stable/rpki/https.py b/rpkid.stable/rpki/https.py new file mode 100644 index 00000000..a0443c01 --- /dev/null +++ b/rpkid.stable/rpki/https.py @@ -0,0 +1,291 @@ +"""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. + +$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. +""" + +import httplib, BaseHTTPServer, tlslite.api, glob, traceback, urlparse, socket, signal +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]) + +def build_https_ta_cache(certs): + """Build a dynamic TLS trust anchor cache.""" + + store = POW.X509Store() + for x in certs: + if rpki.https.debug_tls_certs: + rpki.log.debug("HTTPS dynamic trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) + store.addTrust(x.get_POW()) + return store + +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_https_trust_anchor = None): + """Initialize our modified certificate checker.""" + + self.dynamic_https_trust_anchor = dynamic_https_trust_anchor + + if dynamic_https_trust_anchor 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_https_trust_anchor is not None: + return self.dynamic_https_trust_anchor() + 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_https_trust_anchor = None, catch_signals = (signal.SIGINT, signal.SIGTERM)): + """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_https_trust_anchor = dynamic_https_trust_anchor) + + try: + def raiseServerShuttingDown(signum, frame): + raise rpki.exceptions.ServerShuttingDown + old_signal_handlers = tuple((sig, signal.signal(sig, raiseServerShuttingDown)) for sig in catch_signals) + httpd.serve_forever() + except rpki.exceptions.ServerShuttingDown: + pass + finally: + for sig,handler in old_signal_handlers: + signal.signal(sig, handler) |