libcloud-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From je...@apache.org
Subject svn commit: r1054518 - in /incubator/libcloud/trunk/libcloud: base.py httplib_ssl.py security.py
Date Mon, 03 Jan 2011 02:38:24 GMT
Author: jerry
Date: Mon Jan  3 02:38:23 2011
New Revision: 1054518

URL: http://svn.apache.org/viewvc?rev=1054518&view=rev
Log:
SSL Certificate Name Verification

- libcloud.security
  - libcloud.security.VERIFY_SSL_CERT = False
  - libcloud.security.CA_CERTS_PATH
- LibcloudHTTPSConnection, LibcloudHTTPConnection

Added:
    incubator/libcloud/trunk/libcloud/httplib_ssl.py
    incubator/libcloud/trunk/libcloud/security.py
Modified:
    incubator/libcloud/trunk/libcloud/base.py

Modified: incubator/libcloud/trunk/libcloud/base.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/base.py?rev=1054518&r1=1054517&r2=1054518&view=diff
==============================================================================
--- incubator/libcloud/trunk/libcloud/base.py (original)
+++ incubator/libcloud/trunk/libcloud/base.py Mon Jan  3 02:38:23 2011
@@ -20,15 +20,17 @@ import httplib, urllib
 import libcloud
 from libcloud.types import NodeState, DeploymentError
 from libcloud.ssh import SSHClient
+from libcloud.httplib_ssl import LibcloudHTTPSConnection
+from httplib import HTTPConnection as LibcloudHTTPConnection
 import time
 import hashlib
 import StringIO
+import ssl
 import os
 import socket
 import struct
 from pipes import quote as pquote
 
-
 class Node(object):
     """
     A Base Node class to derive from.
@@ -257,13 +259,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, LibcloudHTTPSConnection):
     """
     Utility Class for logging HTTPS connections
     """
 
     def getresponse(self):
-        r = httplib.HTTPSConnection.getresponse(self)
+        r = LibcloudHTTPSConnection.getresponse(self)
         if self.log is not None:
             r, rv = self._log_response(r)
             self.log.write(rv + "\n")
@@ -277,16 +279,15 @@ class LoggingHTTPSConnection(LoggingConn
             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 LibcloudHTTPSConnection.request(self, method, url, body, headers)
 
-class LoggingHTTPConnection(LoggingConnection, httplib.HTTPConnection):
+class LoggingHTTPConnection(LoggingConnection, LibcloudHTTPConnection):
     """
     Utility Class for logging HTTP connections
     """
 
     def getresponse(self):
-        r = httplib.HTTPConnection.getresponse(self)
+        r = LibcloudHTTPConnection.getresponse(self)
         if self.log is not None:
             r, rv = self._log_response(r)
             self.log.write(rv + "\n")
@@ -300,7 +301,7 @@ class LoggingHTTPConnection(LoggingConne
             self.log.write(pre +
                            self._log_curl(method, url, body, headers) + "\n")
             self.log.flush()
-        return httplib.HTTPConnection.request(self, method, url,
+        return LibcloudHTTPConnection.request(self, method, url,
                                                body, headers)
 
 class ConnectionKey(object):
@@ -315,8 +316,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 = (LibcloudHTTPConnection, LibcloudHTTPSConnection)
 
     responseCls = Response
     connection = None
@@ -355,7 +356,9 @@ 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}
+
+        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 +442,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 ssl.SSLError, e:
+            raise ssl.SSLError(str(e))
+
         response = self.responseCls(self.connection.getresponse())
         response.connection = self
         return response

Added: incubator/libcloud/trunk/libcloud/httplib_ssl.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/httplib_ssl.py?rev=1054518&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/httplib_ssl.py (added)
+++ incubator/libcloud/trunk/libcloud/httplib_ssl.py Mon Jan  3 02:38:23 2011
@@ -0,0 +1,152 @@
+# 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.
+"""
+Subclass for httplib.HTTPSConnection with optional certificate name
+verification, depending on libcloud.security settings.
+"""
+import httplib
+import os
+import re
+import socket
+import ssl
+import warnings
+
+import libcloud.security
+
+class LibcloudHTTPSConnection(httplib.HTTPSConnection):
+    """LibcloudHTTPSConnection
+
+    Subclass of HTTPSConnection which verifies certificate names
+    if and only if CA certificates are available.
+    """
+    verify = False        # does not verify
+    ca_cert = None        # no default CA Certificate
+
+    def __init__(self, *args, **kwargs):
+        """Constructor
+        """
+        self._setup_verify()
+        httplib.HTTPSConnection.__init__(self, *args, **kwargs)
+
+    def _setup_verify(self):
+        """Setup Verify SSL or not
+
+        Reads security module's VERIFY_SSL_CERT and toggles whether
+        the class overrides the connect() class method or runs the
+        inherited httplib.HTTPSConnection connect()
+        """
+        self.verify = libcloud.security.VERIFY_SSL_CERT
+
+        if self.verify:
+            self._setup_ca_cert()
+        else:
+            warnings.warn(libcloud.security.VERIFY_SSL_DISABLED_MSG)
+
+    def _setup_ca_cert(self):
+        """Setup CA Certs
+
+        Search in CA_CERTS_PATH for valid candidates and
+        return first match.  Otherwise, complain about certs
+        not being available.
+        """
+        if not self.verify:
+            return
+
+        ca_certs_available = [cert
+                              for cert in libcloud.security.CA_CERTS_PATH
+                              if os.path.exists(cert)]
+        if ca_certs_available:
+            # use first available certificate
+            self.ca_cert = ca_certs_available[0]
+        else:
+            # no certificates found; toggle verify to False
+            warnings.warn(libcloud.security.CA_CERTS_UNAVAILABLE_MSG)
+            self.ca_cert = None
+            self.verify = False
+
+    def connect(self):
+        """Connect
+
+        Checks if verification is toggled; if not, just call
+        httplib.HTTPSConnection's connect
+        """
+        if not self.verify:
+            return httplib.HTTPSConnection.connect(self)
+
+        # otherwise, create a connection and verify the hostname
+        sock = socket.create_connection((self.host, self.port),
+                                        self.timeout)
+        self.sock = ssl.wrap_socket(sock,
+                                    self.key_file,
+                                    self.cert_file,
+                                    cert_reqs=ssl.CERT_REQUIRED,
+                                    ca_certs=self.ca_cert,
+                                    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):
+        """Verify hostname against peer cert
+
+        Check both commonName and entries in subjectAltName, using a
+        rudimentary glob to dns regex check to find matches
+        """
+        common_name = self._get_common_name(cert)
+        alt_names = self._get_subject_alt_names(cert)
+
+        # replace * with alphanumeric and dash
+        # replace . with literal .
+        valid_patterns = [
+            re.compile(
+                pattern.replace(
+                    r".", r"\."
+                ).replace(
+                    r"*", r"[0-9A-Za-z]+"
+                )
+            )
+            for pattern
+            in (set(common_name) | set(alt_names))
+        ]
+
+        return any(
+            pattern.search(hostname)
+            for pattern in valid_patterns
+        )
+
+    def _get_subject_alt_names(self, cert):
+        """Get SubjectAltNames
+
+        Retrieve 'subjectAltName' attributes from cert data structure
+        """
+        if 'subjectAltName' not in cert:
+            values = []
+        else:
+            values = [value
+                      for field, value in cert['subjectAltName']
+                      if field == 'DNS']
+        return values
+
+    def _get_common_name(self, cert):
+        """Get Common Name
+
+        Retrieve 'commonName' attribute from cert data structure
+        """
+        if 'subject' not in cert:
+            return None
+        values = [value[0][1]
+                  for value in cert['subject']
+                  if value[0][0] == 'commonName']
+        return values

Added: incubator/libcloud/trunk/libcloud/security.py
URL: http://svn.apache.org/viewvc/incubator/libcloud/trunk/libcloud/security.py?rev=1054518&view=auto
==============================================================================
--- incubator/libcloud/trunk/libcloud/security.py (added)
+++ incubator/libcloud/trunk/libcloud/security.py Mon Jan  3 02:38:23 2011
@@ -0,0 +1,51 @@
+# 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.
+"""
+Security (SSL) Settings
+
+Usage:
+    import libcloud.security
+    libcloud.security.VERIFY_SSL_CERT = True
+
+    # optional
+    libcloud.security.CA_CERTS_PATH.append("/path/to/cacert.txt")
+"""
+# 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_PATH = [
+    # centos/fedora: openssl
+    '/etc/pki/tls/certs/ca-bundle.crt',
+
+    # debian/ubuntu/arch/gentoo: ca-certificates
+    '/etc/ssl/certs/ca-certificates.crt',
+
+    # macports: curl-ca-bundle
+    '/opt/local/share/curl/curl-ca-bundle.crt',
+]
+
+CA_CERTS_UNAVAILABLE_MSG = (
+   'Warning: No CA Certificates were found in CA_CERTS_PATH. '
+   'Toggling VERIFY_SSL_CERT to False.'
+)
+
+VERIFY_SSL_DISABLED_MSG = (
+    '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.'
+)



Mime
View raw message