Commit 861563f4 authored by Stephane Bortzmeyer's avatar Stephane Bortzmeyer
Browse files

[DoT] New, RFC6215-compliant host name validation. Closes #11

parent 9f053941
......@@ -75,6 +75,13 @@ def is_valid_hostname(name):
name = str(name.encode('idna').lower())
def is_valid_ip_address(addr):
baddr = netaddr.IPAddress(addr)
except netaddr.core.AddrFormatError:
return False
return True
def is_valid_url(url):
result = urllib.parse.urlparse(url) # A very poor validation, many
......@@ -93,27 +100,44 @@ def get_certificate_san(x509cert):
san = str(ext)
return san
def validate_hostname(hostname, cert):
hostname = hostname.lower()
cn = cert.get_subject().commonName.lower()
if cn.startswith("*."): # Wildcard
(start, base) = cn.split("*.")
if hostname.endswith(base):
# Try one possible name. Names must be already canonicalized.
def match_hostname(hostname, possibleMatch):
print("Testing %s against %s" % (hostname, possibleMatch))
if possibleMatch.startswith("*."): # Wildcard
base = possibleMatch[1:] # Skip the star
# RFC 6125 says that we MAY accept left-most labels with
# wildcards included (foo*bar). We don't do it here.
(first, rest) = hostname.split(".", maxsplit=1)
except ValueError: # One-label name
rest = hostname
if rest == base[1:]:
return True
if hostname == cn:
if hostname == base[1:]:
return True
return False
return hostname == possibleMatch
# Try all the names in the certificate
def validate_hostname(hostname, cert):
# Complete specification is in RFC 6125. It is long and
# complicated and I'm not sure we do it perfectly.
is_addr = is_valid_ip_address(hostname)
hostname = hostname.lower()
hostname = hostname.encode('idna').decode()
for alt_name in get_certificate_san(cert).split(", "):
if alt_name.startswith("DNS:"):
if alt_name.startswith("DNS:") and not is_addr:
(start, base) = alt_name.split("DNS:")
base = base.lower()
if hostname == base:
# We assume the certificate contains only
# A-labels. Otherwise, we would need to: "base =
# str(base.encode('idna'))"
found = match_hostname(hostname, base)
if found:
return True
elif alt_name.startswith("IP Address:"):
elif alt_name.startswith("IP Address:") and is_addr:
host_i = netaddr.IPAddress(hostname)
except netaddr.core.AddrFormatError:
continue # If hostname is not an IP address, we cannot use it for comparison
(start, base) = alt_name.split("IP Address:")
if base.endswith("\n"):
base = base[:-1]
......@@ -121,16 +145,23 @@ def validate_hostname(hostname, cert):
base_i = netaddr.IPAddress(base)
except netaddr.core.AddrFormatError:
continue # Ignore broken IP addresses in certificates. Are we too liberal?
print("Testing %s against %s" % (hostname, base))
if host_i == base_i:
return True
pass # Ignore unknown alternative name types
pass # Ignore unknown alternative name types. May be
# accept URI alternative names for DoH,
# According to RFC 6125, we MUST NOT try the Common Name before the Subject Alternative Names.
cn = cert.get_subject().commonName.lower()
found = match_hostname(hostname, cn)
if found:
return True
return False
class Connection:
def __init__(self, server, servername=None, dot=False, verbose=verbose, insecure=insecure, post=post, head=head):
if dot and not is_valid_hostname(server):
error("DoT requires a host name, not \"%s\"" % server)
error("DoT requires a host name or IP address, not \"%s\"" % server)
if not dot and not is_valid_url(url):
error("DoH requires a valid HTTPS URL, not \"%s\"" % server)
self.server = server
......@@ -166,6 +197,7 @@ class Connection:
self.session = OpenSSL.SSL.Connection(self.context, self.sock)
self.session.set_tlsext_host_name(check.encode()) # Server Name Indication (SNI)
self.session.connect((self.server, 853))
# TODO We may here have exceptions such as OpenSSL.SSL.ZeroReturnError
self.cert = self.session.get_peer_certificate()
if not insecure:
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment