I have fixed the alt_names and licensing issue and added some tests. On Sat, Nov 13, 2010 at 6:13 PM, Jed Smith <[email protected]> wrote:
> 2010/11/12 Tomaž Muraus <[email protected]> > > > I have made a few modification to your patch, namely if M2Crypto library > is > > not available, it uses a custom HTTPS connection module which verifies > the > > server certificate. > > > > The license on httplib_ssl.py means we cannot accept that file. Homogeneous > licensing is necessary as part of our involvement in the Apache Software > Foundation. > > Also, a quick test on my machine resulted in a fairly quick traceback: > > Python 2.7 (r27:82500, Oct 20 2010, 03:21:03) > [GCC 4.5.1] on linux2 > Type "help", "copyright", "credits" or "license" for more information. > >>> from libcloud.drivers.linode import LinodeNodeDriver > >>> z = LinodeNodeDriver("nGDJ.....") > >>> z.list_nodes() > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > File "libcloud/drivers/linode.py", line 232, in list_nodes > data = self.connection.request(LINODE_ROOT, params=params).objects[0] > File "libcloud/base.py", line 484, in request > headers=headers) > File "/usr/lib/python2.7/httplib.py", line 946, in request > self._send_request(method, url, body, headers) > File "/usr/lib/python2.7/httplib.py", line 987, in _send_request > self.endheaders(body) > File "/usr/lib/python2.7/httplib.py", line 940, in endheaders > self._send_output(message_body) > File "/usr/lib/python2.7/httplib.py", line 803, in _send_output > self.send(msg) > File "/usr/lib/python2.7/httplib.py", line 755, in send > self.connect() > File "libcloud/httplib_ssl.py", line 50, in connect > if not self._verify_hostname(self.host, cert): > File "libcloud/httplib_ssl.py", line 57, in _verify_hostname > if (hostname == common_name) or hostname in alt_names: > TypeError: argument of type 'NoneType' is not iterable > >>> > > Looks like alt_names is None? > > J >
From 99327c58ee08d6e21803b48afd2174a174e588df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Muraus?= <[email protected]> Date: Tue, 16 Nov 2010 01:25:12 +0100 Subject: [PATCH] SSL certificate verification patch. --- libcloud/base.py | 64 ++++++++++++++++++++++++++----- libcloud/httplib_ssl.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++ test/test_httplib_ssl.py | 58 ++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 libcloud/httplib_ssl.py create mode 100644 test/test_httplib_ssl.py diff --git a/libcloud/base.py b/libcloud/base.py index f3235c4..60ee798 100644 --- a/libcloud/base.py +++ b/libcloud/base.py @@ -20,14 +20,45 @@ import httplib, urllib import libcloud from libcloud.types import NodeState, DeploymentError from libcloud.ssh import SSHClient +from libcloud.httplib_ssl import VerifiedHTTPSConnection import time import hashlib import StringIO +import ssl import os import socket import struct from pipes import quote as pquote +# For backward compatibility this option is disabled by default +VERIFY_SSL_CERT = False + +# File containing one or more PEM-encoded CA certificates concatenated together +CA_CERTS_FILE_PATH = '/etc/ssl/certs/ca-certificates.crt' + +if VERIFY_SSL_CERT: + try: + from M2Crypto import httpslib + from M2Crypto import SSL + from M2Crypto.SSL import SSLError + + M2CRYPTO = True + HTTPSConnection = httpslib.HTTPSConnection + except ImportError: + # If M2Crypto library is not available custom HTTPS connection module + # which verifies the server certificate is used. + M2CRYPTO = False + SSLError = None + HTTPSConnection = VerifiedHTTPSConnection +else: + HTTPSConnection = httplib.HTTPSConnection + +if not VERIFY_SSL_CERT: + import warnings + warnings.warn('SSL certificate verification is disabled, this can pose a ' + 'security risk. For more information how to enable the SSL ' + 'certificate verification, please visit the libcloud ' + 'documentation.') class Node(object): """ @@ -257,13 +288,13 @@ class LoggingConnection(): cmd.extend([pquote("https://%s:%d%s" % (self.host, self.port, url))]) return " ".join(cmd) -class LoggingHTTPSConnection(LoggingConnection, httplib.HTTPSConnection): +class LoggingHTTPSConnection(LoggingConnection, HTTPSConnection): """ Utility Class for logging HTTPS connections """ def getresponse(self): - r = httplib.HTTPSConnection.getresponse(self) + r = HTTPSConnection.getresponse(self) if self.log is not None: r, rv = self._log_response(r) self.log.write(rv + "\n") @@ -277,8 +308,7 @@ class LoggingHTTPSConnection(LoggingConnection, httplib.HTTPSConnection): self.log.write(pre + self._log_curl(method, url, body, headers) + "\n") self.log.flush() - return httplib.HTTPSConnection.request(self, method, url, - body, headers) + return HTTPSConnection.request(self, method, url, body, headers) class LoggingHTTPConnection(LoggingConnection, httplib.HTTPConnection): """ @@ -315,8 +345,8 @@ class ConnectionKey(object): # with upstream Python (see http://bugs.python.org/issue1589 for details) # and not with libcloud. - #conn_classes = (httplib.LoggingHTTPConnection, LoggingHTTPSConnection) - conn_classes = (httplib.HTTPConnection, httplib.HTTPSConnection) + #conn_classes = (LoggingHTTPSConnection) + conn_classes = (httplib.HTTPConnection, HTTPSConnection) responseCls = Response connection = None @@ -355,7 +385,17 @@ class ConnectionKey(object): host = host or self.host port = port or self.port[self.secure] - connection = self.conn_classes[self.secure](host, port) + kwargs = {'host': host, 'port': port} + if self.secure: + if VERIFY_SSL_CERT and M2CRYPTO: + ssl_context = SSL.Context() + ssl_context.load_verify_info(cafile = CA_CERTS_FILE_PATH) + ssl_context.set_verify(SSL.verify_peer | + SSL.verify_fail_if_no_peer_cert | + SSL.verify_client_once, 20) + kwargs['ssl_context'] = ssl_context + + connection = self.conn_classes[self.secure](**kwargs) # You can uncoment this line, if you setup a reverse proxy server # which proxies to your endpoint, and lets you easily capture # connections in cleartext when you setup the proxy to do SSL @@ -439,8 +479,12 @@ class ConnectionKey(object): # Removed terrible hack...this a less-bad hack that doesn't execute a # request twice, but it's still a hack. self.connect() - self.connection.request(method=method, url=url, body=data, - headers=headers) + try: + self.connection.request(method=method, url=url, body=data, + headers=headers) + except (SSLError, ssl.SSLError), e: + raise ssl.SSLError(str(e)) + response = self.responseCls(self.connection.getresponse()) response.connection = self return response @@ -631,7 +675,7 @@ class NodeDriver(object): or returning a generated password. This function may raise a L{DeplyomentException}, if a create_node - call was successful, but there is a later error (like SSH failing or + call was successful, but there is a later error (like SSH failing or timing out). This exception includes a Node object which you may want to destroy if incomplete deployments are not desirable. diff --git a/libcloud/httplib_ssl.py b/libcloud/httplib_ssl.py new file mode 100644 index 0000000..4909e88 --- /dev/null +++ b/libcloud/httplib_ssl.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re +import socket +import ssl +import httplib +import urllib2 + +class VerifiedHTTPSConnection(httplib.HTTPSConnection): + def connect(self): + from libcloud.base import CA_CERTS_FILE_PATH + + sock = socket.create_connection((self.host, self.port), + self.timeout) + if self._tunnel_host: + self.sock = sock + self._tunnel() + + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, \ + cert_reqs = ssl.CERT_REQUIRED, \ + ca_certs = CA_CERTS_FILE_PATH, \ + ssl_version = ssl.PROTOCOL_TLSv1) + + cert = self.sock.getpeercert() + if not self._verify_hostname(self.host, cert): + raise ssl.SSLError('Failed to verify hostname') + + def _verify_hostname(self, hostname, cert): + common_name = self._get_commonName(cert) + alt_names = self._get_subjectAltName(cert) + + if self._is_wildcard_name(common_name): + regex = self._to_regex(common_name) + + if regex.match(hostname): + return True + else: + if hostname == common_name: + return True + + for alt_name in alt_names: + if self._is_wildcard_name(alt_name): + regex = self._to_regex(alt_name) + + if regex.match(hostname): + return True + else: + if hostname == alt_name: + return True + + return False + + def _get_subjectAltName(self, cert): + if not cert.has_key('subjectAltName'): + return [] + + alt_names = [] + for value in cert['subjectAltName']: + if value[0].lower() == 'dns': + alt_names.append(value[0]) + + return alt_names + + def _get_commonName(self, cert): + if not cert.has_key('subject'): + return None + + for value in cert['subject']: + if value[0][0].lower() == 'commonname': + return value[0][1] + + return None + + def _is_wildcard_name(self, name): + if name.find('*') != -1: + return True + + return False + + def _to_regex(self, name): + regex = name.replace('*', '([^.])+').replace('.', '\.') + + return re.compile(regex) diff --git a/test/test_httplib_ssl.py b/test/test_httplib_ssl.py new file mode 100644 index 0000000..5062ba4 --- /dev/null +++ b/test/test_httplib_ssl.py @@ -0,0 +1,58 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import unittest + +from libcloud.httplib_ssl import VerifiedHTTPSConnection + +class HttpLibTests(unittest.TestCase): + def setUp(self): + self.httplib_object = VerifiedHTTPSConnection('foo.bar') + + def test_is_valid_wildcard_name(self): + self.assertFalse(self.httplib_object._is_wildcard_name('foo.bar')) + self.assertFalse(self.httplib_object._is_wildcard_name('bar.foo')) + self.assertTrue(self.httplib_object._is_wildcard_name('*.foo.bar')) + self.assertTrue(self.httplib_object._is_wildcard_name('*.*.foo.bar')) + + def test_wildcard_match(self): + # Reference: http://www.faqs.org/rfcs/rfc2818.html & + # http://www.faqs.org/rfcs/rfc2459.html + matches_true = [ + ('*.a.com', 'foo.a.com'), + ('f*.com', 'foo.com'), + ('*.*.foo.com', 'bar.foo.foo.com'), + ('*.*.foo.com', 'a.b.foo.com'), + ('*.*.foo.com', 'a.a.foo.com'), + ] + + matches_false = [ + ('*.a.com', 'bar.foo.a.com'), + ('f*.com', 'bar.com'), + ('f*.com', 'barfoo.com'), + ('*.*.foo.com', 'foo.com'), + ('*.*.foo.com', 'bar.foo.com'), + ('*.*.foo.com', '.foo.com'), + ('*.*.foo.com', '.bar.foo.com'), + ] + + for wildcard_name, match_name in matches_true: + regex = self.httplib_object._to_regex(wildcard_name) + self.assertTrue(regex.match(match_name)) + + for wildcard_name, match_name in matches_false: + regex = self.httplib_object._to_regex(wildcard_name) + self.assertFalse(regex.match(match_name)) -- 1.7.3.2
