Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit46632f4

Browse files
[3.7]bpo-32819: Simplify and improve ssl.match_hostname (GH-5620) (#5847)
ssl.match_hostname() has been simplified and no longer depends on re andipaddress module for wildcard and IP addresses. Error reporting for invalidwildcards has been improved.Signed-off-by: Christian Heimes <christian@python.org>(cherry picked from commitaef1283)Co-authored-by: Christian Heimes <christian@python.org>
1 parent1c37e27 commit46632f4

File tree

3 files changed

+117
-57
lines changed

3 files changed

+117
-57
lines changed

‎Lib/ssl.py‎

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@
9090
ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY
9191
"""
9292

93-
importipaddress
94-
importre
9593
importsys
9694
importos
9795
fromcollectionsimportnamedtuple
@@ -160,6 +158,7 @@
160158

161159
fromsocketimportsocket,AF_INET,SOCK_STREAM,create_connection
162160
fromsocketimportSOL_SOCKET,SO_TYPE
161+
importsocketas_socket
163162
importbase64# for DER-to-PEM translation
164163
importerrno
165164
importwarnings
@@ -183,55 +182,75 @@
183182
def_dnsname_match(dn,hostname):
184183
"""Matching according to RFC 6125, section 6.4.3
185184
186-
http://tools.ietf.org/html/rfc6125#section-6.4.3
185+
- Hostnames are compared lower case.
186+
- For IDNA, both dn and hostname must be encoded as IDN A-label (ACE).
187+
- Partial wildcards like 'www*.example.org', multiple wildcards, sole
188+
wildcard or wildcards in labels other then the left-most label are not
189+
supported and a CertificateError is raised.
190+
- A wildcard must match at least one character.
187191
"""
188-
pats= []
189192
ifnotdn:
190193
returnFalse
191194

192-
leftmost,*remainder=dn.split(r'.')
195+
wildcards=dn.count('*')
196+
# speed up common case w/o wildcards
197+
ifnotwildcards:
198+
returndn.lower()==hostname.lower()
199+
200+
ifwildcards>1:
201+
raiseCertificateError(
202+
"too many wildcards in certificate DNS name: {!r}.".format(dn))
193203

194-
wildcards=leftmost.count('*')
195-
ifwildcards==1andlen(leftmost)>1:
204+
dn_leftmost,sep,dn_remainder=dn.partition('.')
205+
206+
if'*'indn_remainder:
196207
# Only match wildcard in leftmost segment.
197208
raiseCertificateError(
198-
"wildcard can only be present in the leftmost segment: "+repr(dn))
209+
"wildcard can only be present in the leftmost label: "
210+
"{!r}.".format(dn))
199211

200-
ifwildcards>1:
201-
# Issue #17980: avoid denials of service by refusing more
202-
# than one wildcard per fragment. A survey of established
203-
# policy among SSL implementations showed it to be a
204-
# reasonable choice.
212+
ifnotsep:
213+
# no right side
205214
raiseCertificateError(
206-
"too many wildcards in certificate DNS name: "+repr(dn))
215+
"sole wildcard without additional labels are not support: "
216+
"{!r}.".format(dn))
207217

208-
# speed up common case w/o wildcards
209-
ifnotwildcards:
210-
returndn.lower()==hostname.lower()
218+
ifdn_leftmost!='*':
219+
# no partial wildcard matching
220+
raiseCertificateError(
221+
"partial wildcards in leftmost label are not supported: "
222+
"{!r}.".format(dn))
211223

212-
# RFC 6125, section 6.4.3, subitem 1.
213-
# The client SHOULD NOT attempt to match a presented identifier in which
214-
# the wildcard character comprises a label other than the left-most label.
215-
ifleftmost=='*':
216-
# When '*' is a fragment by itself, it matches a non-empty dotless
217-
# fragment.
218-
pats.append('[^.]+')
219-
elifleftmost.startswith('xn--')orhostname.startswith('xn--'):
220-
# RFC 6125, section 6.4.3, subitem 3.
221-
# The client SHOULD NOT attempt to match a presented identifier
222-
# where the wildcard character is embedded within an A-label or
223-
# U-label of an internationalized domain name.
224-
pats.append(re.escape(leftmost))
225-
else:
226-
# Otherwise, '*' matches any dotless string, e.g. www*
227-
pats.append(re.escape(leftmost).replace(r'\*','[^.]*'))
224+
hostname_leftmost,sep,hostname_remainder=hostname.partition('.')
225+
ifnothostname_leftmostornotsep:
226+
# wildcard must match at least one char
227+
returnFalse
228+
returndn_remainder.lower()==hostname_remainder.lower()
228229

229-
# add the remaining fragments, ignore any wildcards
230-
forfraginremainder:
231-
pats.append(re.escape(frag))
232230

233-
pat=re.compile(r'\A'+r'\.'.join(pats)+r'\Z',re.IGNORECASE)
234-
returnpat.match(hostname)
231+
def_inet_paton(ipname):
232+
"""Try to convert an IP address to packed binary form
233+
234+
Supports IPv4 addresses on all platforms and IPv6 on platforms with IPv6
235+
support.
236+
"""
237+
# inet_aton() also accepts strings like '1'
238+
ifipname.count('.')==3:
239+
try:
240+
return_socket.inet_aton(ipname)
241+
exceptOSError:
242+
pass
243+
244+
try:
245+
return_socket.inet_pton(_socket.AF_INET6,ipname)
246+
exceptOSError:
247+
raiseValueError("{!r} is neither an IPv4 nor an IP6 "
248+
"address.".format(ipname))
249+
exceptAttributeError:
250+
# AF_INET6 not available
251+
pass
252+
253+
raiseValueError("{!r} is not an IPv4 address.".format(ipname))
235254

236255

237256
def_ipaddress_match(ipname,host_ip):
@@ -241,14 +260,19 @@ def _ipaddress_match(ipname, host_ip):
241260
(section 1.7.2 - "Out of Scope").
242261
"""
243262
# OpenSSL may add a trailing newline to a subjectAltName's IP address
244-
ip=ipaddress.ip_address(ipname.rstrip())
263+
ip=_inet_paton(ipname.rstrip())
245264
returnip==host_ip
246265

247266

248267
defmatch_hostname(cert,hostname):
249268
"""Verify that *cert* (in decoded format as returned by
250269
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
251-
rules are followed, but IP addresses are not accepted for *hostname*.
270+
rules are followed.
271+
272+
The function matches IP addresses rather than dNSNames if hostname is a
273+
valid ipaddress string. IPv4 addresses are supported on all platforms.
274+
IPv6 addresses are supported on platforms with IPv6 support (AF_INET6
275+
and inet_pton).
252276
253277
CertificateError is raised on failure. On success, the function
254278
returns nothing.
@@ -258,7 +282,7 @@ def match_hostname(cert, hostname):
258282
"SSL socket or SSL context with either "
259283
"CERT_OPTIONAL or CERT_REQUIRED")
260284
try:
261-
host_ip=ipaddress.ip_address(hostname)
285+
host_ip=_inet_paton(hostname)
262286
exceptValueError:
263287
# Not an IP address (common case)
264288
host_ip=None

‎Lib/test/test_ssl.py‎

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -622,14 +622,16 @@ def fail(cert, hostname):
622622
fail(cert,'example.net')
623623

624624
# -- IPv6 matching --
625-
cert= {'subject': ((('commonName','example.com'),),),
626-
'subjectAltName': (('DNS','example.com'),
627-
('IP Address','2001:0:0:0:0:0:0:CAFE\n'),
628-
('IP Address','2003:0:0:0:0:0:0:BABA\n'))}
629-
ok(cert,'2001::cafe')
630-
ok(cert,'2003::baba')
631-
fail(cert,'2003::bebe')
632-
fail(cert,'example.net')
625+
ifhasattr(socket,'AF_INET6'):
626+
cert= {'subject': ((('commonName','example.com'),),),
627+
'subjectAltName': (
628+
('DNS','example.com'),
629+
('IP Address','2001:0:0:0:0:0:0:CAFE\n'),
630+
('IP Address','2003:0:0:0:0:0:0:BABA\n'))}
631+
ok(cert,'2001::cafe')
632+
ok(cert,'2003::baba')
633+
fail(cert,'2003::bebe')
634+
fail(cert,'example.net')
633635

634636
# -- Miscellaneous --
635637

@@ -665,14 +667,45 @@ def fail(cert, hostname):
665667

666668
# Issue #17980: avoid denials of service by refusing more than one
667669
# wildcard per fragment.
668-
cert= {'subject': ((('commonName','a*b.com'),),)}
669-
fail(cert,'axxb.com')
670-
cert= {'subject': ((('commonName','a*b.co*'),),)}
671-
fail(cert,'axxb.com')
672-
cert= {'subject': ((('commonName','a*b*.com'),),)}
673-
withself.assertRaises(ssl.CertificateError)ascm:
674-
ssl.match_hostname(cert,'axxbxxc.com')
675-
self.assertIn("too many wildcards",str(cm.exception))
670+
cert= {'subject': ((('commonName','a*b.example.com'),),)}
671+
withself.assertRaisesRegex(
672+
ssl.CertificateError,
673+
"partial wildcards in leftmost label are not supported"):
674+
ssl.match_hostname(cert,'axxb.example.com')
675+
676+
cert= {'subject': ((('commonName','www.*.example.com'),),)}
677+
withself.assertRaisesRegex(
678+
ssl.CertificateError,
679+
"wildcard can only be present in the leftmost label"):
680+
ssl.match_hostname(cert,'www.sub.example.com')
681+
682+
cert= {'subject': ((('commonName','a*b*.example.com'),),)}
683+
withself.assertRaisesRegex(
684+
ssl.CertificateError,
685+
"too many wildcards"):
686+
ssl.match_hostname(cert,'axxbxxc.example.com')
687+
688+
cert= {'subject': ((('commonName','*'),),)}
689+
withself.assertRaisesRegex(
690+
ssl.CertificateError,
691+
"sole wildcard without additional labels are not support"):
692+
ssl.match_hostname(cert,'host')
693+
694+
cert= {'subject': ((('commonName','*.com'),),)}
695+
withself.assertRaisesRegex(
696+
ssl.CertificateError,
697+
r"hostname 'com' doesn't match '\*.com'"):
698+
ssl.match_hostname(cert,'com')
699+
700+
# extra checks for _inet_paton()
701+
forinvalidin ['1','','1.2.3','256.0.0.1','127.0.0.1/24']:
702+
withself.assertRaises(ValueError):
703+
ssl._inet_paton(invalid)
704+
foripaddrin ['127.0.0.1','192.168.0.1']:
705+
self.assertTrue(ssl._inet_paton(ipaddr))
706+
ifhasattr(socket,'AF_INET6'):
707+
foripaddrin ['::1','2001:db8:85a3::8a2e:370:7334']:
708+
self.assertTrue(ssl._inet_paton(ipaddr))
676709

677710
deftest_server_side(self):
678711
# server_hostname doesn't work for server sockets
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ssl.match_hostname() has been simplified and no longer depends on re and
2+
ipaddress module for wildcard and IP addresses. Error reporting for invalid
3+
wildcards has been improved.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2026 Movatter.jp