00001 """HTTPS utilities, both client and server.
00002
00003 At the moment this only knows how to use the PEM certs in my
00004 subversion repository; generalizing it would not be hard, but the more
00005 general version should use SQL anyway.
00006
00007 $Id: https.py 1873 2008-06-12 02:49:41Z sra $
00008
00009 Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00010
00011 Permission to use, copy, modify, and distribute this software for any
00012 purpose with or without fee is hereby granted, provided that the above
00013 copyright notice and this permission notice appear in all copies.
00014
00015 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00016 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00017 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00018 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00019 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00020 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00021 PERFORMANCE OF THIS SOFTWARE.
00022 """
00023
00024 import httplib, BaseHTTPServer, tlslite.api, glob, traceback, urlparse, socket, signal
00025 import rpki.x509, rpki.exceptions, rpki.log
00026
00027
00028 import POW
00029
00030
00031 disable_tls_certificate_validation_exceptions = False
00032
00033
00034 debug_tls_certs = False
00035
00036 rpki_content_type = "application/x-rpki"
00037
00038 def tlslite_certChain(x509):
00039 """Utility function to construct tlslite certChains."""
00040 if isinstance(x509, rpki.x509.X509):
00041 return tlslite.api.X509CertChain([x509.get_tlslite()])
00042 else:
00043 return tlslite.api.X509CertChain([x.get_tlslite() for x in x509])
00044
00045 def build_https_ta_cache(certs):
00046 """Build a dynamic TLS trust anchor cache."""
00047
00048 store = POW.X509Store()
00049 for x in certs:
00050 if rpki.https.debug_tls_certs:
00051 rpki.log.debug("HTTPS dynamic trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI()))
00052 store.addTrust(x.get_POW())
00053 return store
00054
00055 class Checker(tlslite.api.Checker):
00056 """Derived class to handle X.509 client certificate checking."""
00057
00058
00059
00060
00061
00062 refuse_tls_ca_certs = False
00063
00064
00065
00066
00067 pem_dump_tls_certs = False
00068
00069 def __init__(self, trust_anchor = None, dynamic_https_trust_anchor = None):
00070 """Initialize our modified certificate checker."""
00071
00072 self.dynamic_https_trust_anchor = dynamic_https_trust_anchor
00073
00074 if dynamic_https_trust_anchor is not None:
00075 return
00076
00077 self.x509store = POW.X509Store()
00078
00079 trust_anchor = rpki.x509.X509.normalize_chain(trust_anchor)
00080 assert trust_anchor
00081
00082 for x in trust_anchor:
00083 if debug_tls_certs:
00084 rpki.log.debug("HTTPS trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI()))
00085 self.x509store.addTrust(x.get_POW())
00086 if self.pem_dump_tls_certs:
00087 print x.get_PEM()
00088
00089 def x509store_thunk(self):
00090 if self.dynamic_https_trust_anchor is not None:
00091 return self.dynamic_https_trust_anchor()
00092 else:
00093 return self.x509store
00094
00095 def __call__(self, tlsConnection):
00096 """POW/OpenSSL-based certificate checker.
00097
00098 Given our BPKI model, we're only interested in the TLS EE
00099 certificates.
00100 """
00101
00102 if tlsConnection._client:
00103 chain = tlsConnection.session.serverCertChain
00104 peer = "server"
00105 else:
00106 chain = tlsConnection.session.clientCertChain
00107 peer = "client"
00108
00109 chain = [rpki.x509.X509(tlslite = chain.x509List[i]) for i in range(chain.getNumCerts())]
00110
00111 ee = None
00112
00113 for x in chain:
00114
00115 if debug_tls_certs:
00116 rpki.log.debug("Received %s TLS %s cert issuer %s [%s] subject %s [%s]"
00117 % (peer, "CA" if x.is_CA() else "EE", x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI()))
00118 if self.pem_dump_tls_certs:
00119 print x.get_PEM()
00120
00121 if x.is_CA():
00122 if self.refuse_tls_ca_certs:
00123 raise rpki.exceptions.ReceivedTLSCACert
00124 continue
00125
00126 if ee is not None:
00127 raise rpki.exceptions.MultipleTLSEECert, chain
00128 ee = x
00129
00130 result = self.x509store_thunk().verifyDetailed(ee.get_POW())
00131 if not result[0]:
00132 rpki.log.debug("TLS certificate validation result %s" % repr(result))
00133 if disable_tls_certificate_validation_exceptions:
00134 rpki.log.warn("DANGER WILL ROBINSON! IGNORING TLS VALIDATION FAILURE!")
00135 else:
00136 raise rpki.exceptions.TLSValidationError
00137
00138 class httpsClient(tlslite.api.HTTPTLSConnection):
00139 """Derived class to let us replace the default Checker."""
00140
00141 def __init__(self, host, port = None,
00142 client_cert = None, client_key = None,
00143 server_ta = None, settings = None):
00144 """Create a new httpsClient."""
00145
00146 tlslite.api.HTTPTLSConnection.__init__(
00147 self, host = host, port = port, settings = settings,
00148 certChain = client_cert, privateKey = client_key)
00149
00150 self.checker = Checker(trust_anchor = server_ta)
00151
00152 def client(msg, client_key, client_cert, server_ta, url, timeout = 300):
00153 """Open client HTTPS connection, send a message, wait for response.
00154
00155 This function wraps most of what one needs to do to send a message
00156 over HTTPS and get a response. The certificate checking isn't quite
00157 up to snuff; it's better than with the other packages I've found,
00158 but doesn't appear to handle subjectAltName extensions (sigh).
00159 """
00160
00161 u = urlparse.urlparse(url)
00162
00163 assert u.scheme in ("", "https") and \
00164 u.username is None and \
00165 u.password is None and \
00166 u.params == "" and \
00167 u.query == "" and \
00168 u.fragment == ""
00169
00170 rpki.log.debug("Contacting %s" % url)
00171
00172 if debug_tls_certs:
00173 for cert in (client_cert,) if isinstance(client_cert, rpki.x509.X509) else client_cert:
00174 rpki.log.debug("Sending client TLS cert issuer %s subject %s" % (cert.getIssuer(), cert.getSubject()))
00175
00176
00177
00178
00179
00180 httpc = httpsClient(host = u.hostname or "localhost",
00181 port = u.port or 443,
00182 client_key = client_key.get_tlslite(),
00183 client_cert = tlslite_certChain(client_cert),
00184 server_ta = server_ta)
00185 httpc.connect()
00186 httpc.sock.settimeout(timeout)
00187 httpc.request("POST", u.path, msg, {"Content-Type" : rpki_content_type})
00188 response = httpc.getresponse()
00189 if response.status == httplib.OK:
00190 return response.read()
00191 else:
00192 r = response.read()
00193 raise rpki.exceptions.HTTPRequestFailed, \
00194 "HTTP request failed with status %s, response %s" % (response.status, r)
00195
00196 class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
00197 """Derived type to supply POST handler and override logging."""
00198
00199 rpki_handlers = None
00200
00201 def rpki_find_handler(self):
00202 """Helper method to search self.rpki_handlers."""
00203 for s,h in self.rpki_handlers:
00204 if self.path.startswith(s):
00205 return h
00206 return None
00207
00208 def do_POST(self):
00209 """POST handler."""
00210 try:
00211 handler = self.rpki_find_handler()
00212 if self.headers["Content-Type"] != rpki_content_type:
00213 rcode, rtext = 415, "Received Content-Type %s, expected %s" \
00214 % (self.headers["Content-Type"], rpki_content_type)
00215 elif handler is None:
00216 rcode, rtext = 404, "No handler found for URL " + self.path
00217 else:
00218 rcode, rtext = handler(query = self.rfile.read(int(self.headers["Content-Length"])),
00219 path = self.path)
00220 except Exception, edata:
00221 rpki.log.error(traceback.format_exc())
00222 rcode, rtext = 500, "Unhandled exception %s" % edata
00223 self.send_response(rcode)
00224 self.send_header("Content-Type", rpki_content_type)
00225 self.end_headers()
00226 self.wfile.write(rtext)
00227
00228 def log_message(self, format, *args):
00229 """Redirect HTTP server logging into our own logging system."""
00230 if args:
00231 rpki.log.info(format % args)
00232 else:
00233 rpki.log.info(format)
00234
00235 class httpsServer(tlslite.api.TLSSocketServerMixIn, BaseHTTPServer.HTTPServer):
00236 """Derived type to handle TLS aspects of HTTPS."""
00237
00238 rpki_sessionCache = None
00239 rpki_server_key = None
00240 rpki_server_cert = None
00241 rpki_checker = None
00242
00243 def handshake(self, tlsConnection):
00244 """TLS handshake handler."""
00245 assert self.rpki_server_cert is not None
00246 assert self.rpki_server_key is not None
00247 assert self.rpki_sessionCache is not None
00248
00249 try:
00250
00251
00252
00253
00254
00255 tlsConnection.handshakeServer(certChain = self.rpki_server_cert,
00256 privateKey = self.rpki_server_key,
00257 sessionCache = self.rpki_sessionCache,
00258 checker = self.rpki_checker,
00259 reqCert = True)
00260 tlsConnection.ignoreAbruptClose = True
00261 return True
00262 except (tlslite.api.TLSError, rpki.exceptions.TLSValidationError), error:
00263 rpki.log.warn("TLS handshake failure: " + str(error))
00264 return False
00265
00266 def server(handlers, server_key, server_cert, port = 4433, host ="", client_ta = None, dynamic_https_trust_anchor = None, catch_signals = (signal.SIGINT, signal.SIGTERM)):
00267 """Run an HTTPS server and wait (forever) for connections."""
00268
00269 if not isinstance(handlers, (tuple, list)):
00270 handlers = (("/", handlers),)
00271
00272 class boundRequestHandler(requestHandler):
00273 rpki_handlers = handlers
00274
00275 httpd = httpsServer((host, port), boundRequestHandler)
00276
00277 httpd.rpki_server_key = server_key.get_tlslite()
00278 httpd.rpki_server_cert = tlslite_certChain(server_cert)
00279 httpd.rpki_sessionCache = tlslite.api.SessionCache()
00280 httpd.rpki_checker = Checker(trust_anchor = client_ta, dynamic_https_trust_anchor = dynamic_https_trust_anchor)
00281
00282 try:
00283 def raiseServerShuttingDown(signum, frame):
00284 raise rpki.exceptions.ServerShuttingDown
00285 old_signal_handlers = tuple((sig, signal.signal(sig, raiseServerShuttingDown)) for sig in catch_signals)
00286 httpd.serve_forever()
00287 except rpki.exceptions.ServerShuttingDown:
00288 pass
00289 finally:
00290 for sig,handler in old_signal_handlers:
00291 signal.signal(sig, handler)