From: <mi...@us...> - 2014-01-17 14:11:07
|
Revision: 627 http://sourceforge.net/p/pywbem/code/627 Author: miminar Date: 2014-01-17 14:11:05 +0000 (Fri, 17 Jan 2014) Log Message: ----------- fixed TOCTOU error when validating peer's certificate By TOCTOU it's meant time-of-check-time-of-use. Up to now, pywbem made two connections for one request (applies just to ssl). The first one made the verification (without the hostname check) and the second one was used for request. No verification was done for the latter, which could be abused. Peer's certificate is now validated when connecting over ssl. To prevent man-in-the-middle attack, verification of hostname is also added. Peer's hostname must match the commonName of its certificate. Or it must be contained in subjectAltName (list of aliases). M2Crypto package is used for that purpose. Thanks to it both security enhancements could be implemented quiete easily. Downside is a new dependency added to pywbem. Verification can be skipped if no_verification is set to False. Certificate trust store can now be specified by user. Some default paths, valid for several distributions, were added. Modified Paths: -------------- pywbem/trunk/cim_http.py pywbem/trunk/cim_operations.py Modified: pywbem/trunk/cim_http.py =================================================================== --- pywbem/trunk/cim_http.py 2013-11-21 13:46:57 UTC (rev 626) +++ pywbem/trunk/cim_http.py 2014-01-17 14:11:05 UTC (rev 627) @@ -28,6 +28,7 @@ data and interpret the result. ''' +from M2Crypto import SSL, Err import sys, string, re, os, socket, getpass from stat import S_ISSOCK import cim_obj @@ -74,8 +75,26 @@ return host, port, ssl +def get_default_ca_certs(): + """ + Try to find out system path with ca certificates. This path is cached and + returned. If no path is found out, None is returned. + """ + if not hasattr(get_default_ca_certs, '_path'): + for path in ( + '/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt', + '/etc/ssl/certs', + '/etc/ssl/certificates'): + if os.path.exists(path): + get_default_ca_certs._path = path + break + else: + get_default_ca_certs._path = None + return get_default_ca_certs._path + def wbem_request(url, data, creds, headers = [], debug = 0, x509 = None, - verify_callback = None): + verify_callback = None, ca_certs = None, + no_verification = False): """Send XML data over HTTP to the specified url. Return the response in XML. Uses Python's build-in httplib. x509 may be a dictionary containing the location of the SSL certificate and key @@ -105,10 +124,49 @@ class HTTPSConnection(HTTPBaseConnection, httplib.HTTPSConnection): def __init__(self, host, port=None, key_file=None, cert_file=None, - strict=None): + strict=None, ca_certs=None, verify_callback=None): httplib.HTTPSConnection.__init__(self, host, port, key_file, cert_file, strict) - + self.ca_certs = ca_certs + self.verify_callback = verify_callback + + def connect(self): + "Connect to a host on a given (SSL) port." + self.sock = socket.create_connection((self.host, self.port), + self.timeout, self.source_address) + if self._tunnel_host: + self.sock = sock + self._tunnel() + ctx = SSL.Context('sslv23') + if self.cert_file: + ctx.load_cert(self.cert_file, keyfile=self.key_file) + if self.ca_certs: + ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert, + depth=9, callback=verify_callback) + if os.path.isdir(self.ca_certs): + ctx.load_verify_locations(capath=self.ca_certs) + else: + ctx.load_verify_locations(cafile=self.ca_certs) + try: + self.sock = SSL.Connection(ctx, self.sock) + # Below is a body of SSL.Connection.connect() method + # except for the first line (socket connection). We want to preserve + # tunneling ability. + self.sock.addr = (self.host, self.port) + self.sock.setup_ssl() + self.sock.set_connect_state() + ret = self.sock.connect_ssl() + if self.ca_certs: + check = getattr(self.sock, 'postConnectionCheck', + self.sock.clientPostConnectionCheck) + if check is not None: + if not check(self.sock.get_peer_cert(), self.host): + raise Error('SSL error: post connection check failed') + return ret + except ( Err.SSLError, SSL.SSLError, SSL.SSLTimeoutError + , SSL.Checker.WrongHost), arg: + raise Error("SSL error: %s" % arg) + class FileHTTPConnection(HTTPBaseConnection, httplib.HTTPConnection): def __init__(self, uds_path): httplib.HTTPConnection.__init__(self, 'localhost') @@ -117,64 +175,36 @@ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(self.uds_path) - host, port, ssl = parse_url(url) + host, port, use_ssl = parse_url(url) key_file = None cert_file = None - if ssl: + if use_ssl and x509 is not None: + cert_file = x509.get('cert_file') + key_file = x509.get('key_file') - if x509 is not None: - cert_file = x509.get('cert_file') - key_file = x509.get('key_file') - - if verify_callback is not None: - addr_ind = 0 - # Temporary exception store - addr_exc = None - # Get a list of arguments for socket(). - addr_list = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) - for addr_ind in xrange(len(addr_list)): - family, socktype, proto, canonname, sockaddr = addr_list[addr_ind] - try: - from OpenSSL import SSL - ctx = SSL.Context(SSL.SSLv3_METHOD) - ctx.set_verify(SSL.VERIFY_PEER, verify_callback) - ctx.set_default_verify_paths() - # Add the key and certificate to the session - if cert_file is not None and key_file is not None: - ctx.use_certificate_file(cert_file) - ctx.use_privatekey_file(key_file) - s = SSL.Connection(ctx, socket.socket(family, socktype, proto)) - s.connect((host, port)) - s.do_handshake() - s.shutdown() - s.close() - addr_exc = None - break - except (socket.gaierror, socket.error), arg: - # Could not perform connect() call, store the exception object for - # later use. - addr_exc = arg - continue - except socket.sslerror, arg: - raise Error("SSL error: %s" % (arg,)) - - # Did we try all the addresses from getaddrinfo() and no successful - # connection performed? - if addr_exc: - raise Error("Socket error: %s" % (addr_exc),) - numTries = 0 localAuthHeader = None tryLimit = 5 + if isinstance(data, unicode): + data = data.encode('utf-8') data = '<?xml version="1.0" encoding="utf-8" ?>\n' + data + if not no_verification and ca_certs is None: + ca_certs = get_default_ca_certs() + elif no_verification: + ca_certs = None + local = False - if ssl: - h = HTTPSConnection(host, port = port, key_file = key_file, - cert_file = cert_file) + if use_ssl: + h = HTTPSConnection(host, + port = port, + key_file = key_file, + cert_file = cert_file, + ca_certs = ca_certs, + verify_callback = verify_callback) else: if url.startswith('http'): h = HTTPConnection(host, port = port) @@ -216,6 +246,8 @@ h.putheader('PegasusAuthorization', 'Local "%s"' % locallogin) for hdr in headers: + if isinstance(hdr, unicode): + hdr = hdr.encode('utf-8') s = map(lambda x: string.strip(x), string.split(hdr, ":", 1)) h.putheader(urllib.quote(s[0]), urllib.quote(s[1])) Modified: pywbem/trunk/cim_operations.py =================================================================== --- pywbem/trunk/cim_operations.py 2013-11-21 13:46:57 UTC (rev 626) +++ pywbem/trunk/cim_operations.py 2014-01-17 14:11:05 UTC (rev 627) @@ -27,7 +27,7 @@ from types import StringTypes from xml.dom import minidom import cim_obj, cim_xml, cim_http, cim_types -from cim_obj import CIMClassName, CIMInstanceName, CIMInstance, CIMClass, NocaseDict +from cim_obj import CIMClassName, CIMInstanceName, CIMInstance, CIMClass from datetime import datetime, timedelta from tupletree import dom_to_tupletree, xml_to_tupletree from tupleparse import parse_cim @@ -78,12 +78,12 @@ the request before it is sent, and the reply before it is unpacked. - verify_callback is used to verify the server certificate. - It is passed to OpenSSL.SSL.set_verify, and is called during the SSL - handshake. verify_callback should take five arguments: A Connection - object, an X509 object, and three integer variables, which are in turn - potential error number, error depth and return code. verify_callback - should return True if verification passes and False otherwise. + verify_callback is used to verify the server certificate. It is passed to + M2Crypto.SSL.Context.set_verify, and is called during the SSL handshake. + verify_callback should take five arguments: An SSL Context object, an X509 + object, and three integer variables, which are in turn potential error + number, error depth and return code. verify_callback should return True if + verification passes and False otherwise. The value of the x509 argument is used only when the url contains 'https'. x509 must be a dictionary containing the keys 'cert_file' @@ -91,14 +91,27 @@ filename of an certificate and the value of 'key_file' must consist of a filename containing the private key belonging to the public key that is part of the certificate in cert_file. + + ca_certs specifies where CA certificates for verification purposes are + located. These are trusted certificates. Note that the certificates have to + be in PEM format. Either it is a directory prepared using the c_rehash tool + included with OpenSSL or an pemfile. If None, default system path will be + used. + + no_verification allows to disable peer's verification. This is insecure and + should be avoided. If True, peer's certificate is not verified and ca_certs + argument is ignored. """ def __init__(self, url, creds = None, default_namespace = DEFAULT_NAMESPACE, - x509 = None, verify_callback = None): + x509 = None, verify_callback = None, ca_certs = None, + no_verification = False): self.url = url self.creds = creds self.x509 = x509 self.verify_callback = verify_callback + self.ca_certs = ca_certs + self.no_verification = no_verification self.last_request = self.last_reply = '' self.default_namespace = default_namespace self.debug = False @@ -164,7 +177,9 @@ resp_xml = cim_http.wbem_request(self.url, req_xml.toxml(), self.creds, headers, x509 = self.x509, - verify_callback = self.verify_callback) + verify_callback = self.verify_callback, + ca_certs = self.ca_certs, + no_verification = self.no_verification) except cim_http.AuthError: raise except cim_http.Error, arg: @@ -321,7 +336,9 @@ resp_xml = cim_http.wbem_request(self.url, req_xml.toxml(), self.creds, headers, x509 = self.x509, - verify_callback = self.verify_callback) + verify_callback = self.verify_callback, + ca_certs = self.ca_certs, + no_verification = self.no_verification) except cim_http.Error, arg: # Convert cim_http exceptions to CIMError exceptions raise CIMError(0, str(arg)) @@ -811,7 +828,7 @@ # Convert zero or more PARAMVALUE elements into dictionary - output_params = NocaseDict() + output_params = {} for p in result: if p[1] == 'reference': This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |