blob: aac35ff7de7929b02a8f66825211aa129d92c422 [file] [log] [blame]
# 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 os
import re
import socket
import ssl
import warnings
import libcloud.security
from libcloud.utils.py3 import httplib
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
self.strict = libcloud.security.VERIFY_SSL_CERT_STRICT
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:
if self.strict:
raise RuntimeError(
libcloud.security.CA_CERTS_UNAVAILABLE_ERROR_MSG)
else:
# no certificates found; toggle verify to False
warnings.warn(
libcloud.security.CA_CERTS_UNAVAILABLE_WARNING_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
# use socket.create_connection (in 2.6+) if possible
if getattr(socket, 'create_connection', None):
sock = socket.create_connection((self.host, self.port),
self.timeout)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
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 .
# http://www.dns.net/dnsrd/trick.html#legal-hostnames
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