Skip to content

Commit

Permalink
Use ssl to verify hostname part 2
Browse files Browse the repository at this point in the history
Python 3.12 removes `ssl.match_hostname()` as it was deprecated in 3.7 in favor
of `SSLContext.check_hostname` flag.

This commit therefore enables native ssl's check_hostname if available (3.7+)
and reverts 041d9ac to also check hostname
without relying on availability of both - just in case.
  • Loading branch information
deathaxe committed Feb 10, 2024
1 parent 2143ef2 commit d1fdaad
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 8 deletions.
3 changes: 3 additions & 0 deletions package_control/downloaders/urllib_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 39 additions & 8 deletions package_control/http/validating_https_connection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import base64
import hashlib
import os
import re
import socket
import ssl

Expand Down Expand Up @@ -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

Expand All @@ -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'):
Expand Down Expand Up @@ -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)

0 comments on commit d1fdaad

Please sign in to comment.