diff --git a/package_control/downloaders/urllib_downloader.py b/package_control/downloaders/urllib_downloader.py index 63cb402a..9455a91b 100644 --- a/package_control/downloaders/urllib_downloader.py +++ b/package_control/downloaders/urllib_downloader.py @@ -124,6 +124,9 @@ def download(self, url, error_message, timeout, tries, prefer_cached=False): return self.cache_result('get', url, http_file.getcode(), http_file.headers, result) + except (ssl.CertificateError) as e: + error_string = 'Certificate validation for %s failed: %s' % (url, str(e)) + except (HTTPException) as e: # Since we use keep-alives, it is possible the other end closed # the connection, and we may just need to re-open diff --git a/package_control/http/validating_https_connection.py b/package_control/http/validating_https_connection.py index 4b3ac8c0..714c111e 100644 --- a/package_control/http/validating_https_connection.py +++ b/package_control/http/validating_https_connection.py @@ -1,6 +1,7 @@ import base64 import hashlib import os +import re import socket import ssl @@ -51,7 +52,7 @@ def __init__(self, host, port=None, ca_certs=None, extra_ca_certs=None, **kwargs context.verify_mode = ssl.CERT_REQUIRED if hasattr(context, 'check_hostname'): - context.check_hostname = False + context.check_hostname = True if hasattr(context, 'post_handshake_auth'): context.post_handshake_auth = True @@ -72,6 +73,38 @@ def __init__(self, host, port=None, ca_certs=None, extra_ca_certs=None, **kwargs self._context = context + def get_valid_hosts_for_cert(self, cert): + """ + Returns a list of valid hostnames for an SSL certificate + + :param cert: A dict from SSLSocket.getpeercert() + + :return: An array of hostnames + """ + + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] if x[0][0].lower() == 'commonname'] + + def validate_cert_host(self, cert, hostname): + """ + Checks if the cert is valid for the hostname + + :param cert: A dict from SSLSocket.getpeercert() + + :param hostname: A string hostname to check + + :return: A boolean if the cert is valid for the hostname + """ + + hosts = self.get_valid_hosts_for_cert(cert) + for host in hosts: + host_re = host.replace('.', r'\.').replace('*', r'[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + # Compatibility for python 3.3 vs 3.8 # python 3.8 replaced _set_hostport() by _get_hostport() if not hasattr(DebuggableHTTPConnection, '_set_hostport'): @@ -360,13 +393,11 @@ def connect(self): if 'notAfter' in cert: console_write(' expire date: %s', cert['notAfter'], prefix=False) - try: - ssl.match_hostname(cert, hostname) + if not self.validate_cert_host(cert, hostname): if self.debuglevel == -1: - console_write(' Certificate validated for %s', hostname, prefix=False) + console_write(' Certificate INVALID', prefix=False) - except ssl.CertificateError as e: - if self.debuglevel == -1: - console_write(' Certificate INVALID: %s', e, prefix=False) + raise InvalidCertificateException(hostname, cert, 'hostname mismatch') - raise InvalidCertificateException(hostname, cert, e) + if self.debuglevel == -1: + console_write(' Certificate validated for %s', hostname, prefix=False)