Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 65 additions & 41 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@
ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
"""

import ipaddress
import re
import sys
import os
from collections import namedtuple
Expand Down Expand Up @@ -160,6 +158,7 @@

from socket import socket, AF_INET, SOCK_STREAM, create_connection
from socket import SOL_SOCKET, SO_TYPE
import socket as _socket
import base64 # for DER-to-PEM translation
import errno
import warnings
Expand All @@ -183,55 +182,75 @@
def _dnsname_match(dn, hostname):
"""Matching according to RFC 6125, section 6.4.3

http://tools.ietf.org/html/rfc6125#section-6.4.3
- Hostnames are compared lower case.
- For IDNA, both dn and hostname must be encoded as IDN A-label (ACE).
- Partial wildcards like 'www*.example.org', multiple wildcards, sole
wildcard or wildcards in labels other then the left-most label are not
supported and a CertificateError is raised.
- A wildcard must match at least one character.
"""
pats = []
if not dn:
return False

leftmost, *remainder = dn.split(r'.')
wildcards = dn.count('*')
# speed up common case w/o wildcards
if not wildcards:
return dn.lower() == hostname.lower()

if wildcards > 1:
raise CertificateError(
"too many wildcards in certificate DNS name: {!r}.".format(dn))

wildcards = leftmost.count('*')
if wildcards == 1 and len(leftmost) > 1:
dn_leftmost, sep, dn_remainder = dn.partition('.')

if '*' in dn_remainder:
# Only match wildcard in leftmost segment.
raise CertificateError(
"wildcard can only be present in the leftmost segment: " + repr(dn))
"wildcard can only be present in the leftmost label: "
"{!r}.".format(dn))

if wildcards > 1:
# Issue #17980: avoid denials of service by refusing more
# than one wildcard per fragment. A survey of established
# policy among SSL implementations showed it to be a
# reasonable choice.
if not sep:
# no right side
raise CertificateError(
"too many wildcards in certificate DNS name: " + repr(dn))
"sole wildcard without additional labels are not support: "
"{!r}.".format(dn))

# speed up common case w/o wildcards
if not wildcards:
return dn.lower() == hostname.lower()
if dn_leftmost != '*':
# no partial wildcard matching
raise CertificateError(
"partial wildcards in leftmost label are not supported: "
"{!r}.".format(dn))

# RFC 6125, section 6.4.3, subitem 1.
# The client SHOULD NOT attempt to match a presented identifier in which
# the wildcard character comprises a label other than the left-most label.
if leftmost == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
# RFC 6125, section 6.4.3, subitem 3.
# The client SHOULD NOT attempt to match a presented identifier
# where the wildcard character is embedded within an A-label or
# U-label of an internationalized domain name.
pats.append(re.escape(leftmost))
else:
# Otherwise, '*' matches any dotless string, e.g. www*
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
hostname_leftmost, sep, hostname_remainder = hostname.partition('.')
if not hostname_leftmost or not sep:
# wildcard must match at least one char
return False
return dn_remainder.lower() == hostname_remainder.lower()

# add the remaining fragments, ignore any wildcards
for frag in remainder:
pats.append(re.escape(frag))

pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
return pat.match(hostname)
def _inet_paton(ipname):
"""Try to convert an IP address to packed binary form

Supports IPv4 addresses on all platforms and IPv6 on platforms with IPv6
support.
"""
# inet_aton() also accepts strings like '1'
if ipname.count('.') == 3:
try:
return _socket.inet_aton(ipname)
except OSError:
pass

try:
return _socket.inet_pton(_socket.AF_INET6, ipname)
except OSError:
raise ValueError("{!r} is neither an IPv4 nor an IP6 "
"address.".format(ipname))
except AttributeError:
# AF_INET6 not available
pass

raise ValueError("{!r} is not an IPv4 address.".format(ipname))


def _ipaddress_match(ipname, host_ip):
Expand All @@ -241,14 +260,19 @@ def _ipaddress_match(ipname, host_ip):
(section 1.7.2 - "Out of Scope").
"""
# OpenSSL may add a trailing newline to a subjectAltName's IP address
ip = ipaddress.ip_address(ipname.rstrip())
ip = _inet_paton(ipname.rstrip())
return ip == host_ip


def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
rules are followed, but IP addresses are not accepted for *hostname*.
rules are followed.

The function matches IP addresses rather than dNSNames if hostname is a
valid ipaddress string. IPv4 addresses are supported on all platforms.
IPv6 addresses are supported on platforms with IPv6 support (AF_INET6
and inet_pton).

CertificateError is raised on failure. On success, the function
returns nothing.
Expand All @@ -258,7 +282,7 @@ def match_hostname(cert, hostname):
"SSL socket or SSL context with either "
"CERT_OPTIONAL or CERT_REQUIRED")
try:
host_ip = ipaddress.ip_address(hostname)
host_ip = _inet_paton(hostname)
except ValueError:
# Not an IP address (common case)
host_ip = None
Expand Down
65 changes: 49 additions & 16 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,14 +622,16 @@ def fail(cert, hostname):
fail(cert, 'example.net')

# -- IPv6 matching --
cert = {'subject': ((('commonName', 'example.com'),),),
'subjectAltName': (('DNS', 'example.com'),
('IP Address', '2001:0:0:0:0:0:0:CAFE\n'),
('IP Address', '2003:0:0:0:0:0:0:BABA\n'))}
ok(cert, '2001::cafe')
ok(cert, '2003::baba')
fail(cert, '2003::bebe')
fail(cert, 'example.net')
if hasattr(socket, 'AF_INET6'):
cert = {'subject': ((('commonName', 'example.com'),),),
'subjectAltName': (
('DNS', 'example.com'),
('IP Address', '2001:0:0:0:0:0:0:CAFE\n'),
('IP Address', '2003:0:0:0:0:0:0:BABA\n'))}
ok(cert, '2001::cafe')
ok(cert, '2003::baba')
fail(cert, '2003::bebe')
fail(cert, 'example.net')

# -- Miscellaneous --

Expand Down Expand Up @@ -665,14 +667,45 @@ def fail(cert, hostname):

# Issue #17980: avoid denials of service by refusing more than one
# wildcard per fragment.
cert = {'subject': ((('commonName', 'a*b.com'),),)}
fail(cert, 'axxb.com')
cert = {'subject': ((('commonName', 'a*b.co*'),),)}
fail(cert, 'axxb.com')
cert = {'subject': ((('commonName', 'a*b*.com'),),)}
with self.assertRaises(ssl.CertificateError) as cm:
ssl.match_hostname(cert, 'axxbxxc.com')
self.assertIn("too many wildcards", str(cm.exception))
cert = {'subject': ((('commonName', 'a*b.example.com'),),)}
with self.assertRaisesRegex(
ssl.CertificateError,
"partial wildcards in leftmost label are not supported"):
ssl.match_hostname(cert, 'axxb.example.com')

cert = {'subject': ((('commonName', 'www.*.example.com'),),)}
with self.assertRaisesRegex(
ssl.CertificateError,
"wildcard can only be present in the leftmost label"):
ssl.match_hostname(cert, 'www.sub.example.com')

cert = {'subject': ((('commonName', 'a*b*.example.com'),),)}
with self.assertRaisesRegex(
ssl.CertificateError,
"too many wildcards"):
ssl.match_hostname(cert, 'axxbxxc.example.com')

cert = {'subject': ((('commonName', '*'),),)}
with self.assertRaisesRegex(
ssl.CertificateError,
"sole wildcard without additional labels are not support"):
ssl.match_hostname(cert, 'host')

cert = {'subject': ((('commonName', '*.com'),),)}
with self.assertRaisesRegex(
ssl.CertificateError,
r"hostname 'com' doesn't match '\*.com'"):
ssl.match_hostname(cert, 'com')

# extra checks for _inet_paton()
for invalid in ['1', '', '1.2.3', '256.0.0.1', '127.0.0.1/24']:
with self.assertRaises(ValueError):
ssl._inet_paton(invalid)
for ipaddr in ['127.0.0.1', '192.168.0.1']:
self.assertTrue(ssl._inet_paton(ipaddr))
if hasattr(socket, 'AF_INET6'):
for ipaddr in ['::1', '2001:db8:85a3::8a2e:370:7334']:
self.assertTrue(ssl._inet_paton(ipaddr))

def test_server_side(self):
# server_hostname doesn't work for server sockets
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ssl.match_hostname() has been simplified and no longer depends on re and
ipaddress module for wildcard and IP addresses. Error reporting for invalid
wildcards has been improved.