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

Reply via email to