##// END OF EJS Templates
sslutil: synchronize hostname matching logic with CPython...
Gregory Szorc -
r29452:26a5d605 3.8.4 stable
parent child Browse files
Show More
@@ -10,6 +10,7 b''
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import os
12 import os
13 import re
13 import ssl
14 import ssl
14 import sys
15 import sys
15
16
@@ -167,6 +168,57 b' def wrapsocket(sock, keyfile, certfile, '
167 raise error.Abort(_('ssl connection failed'))
168 raise error.Abort(_('ssl connection failed'))
168 return sslsocket
169 return sslsocket
169
170
171 class wildcarderror(Exception):
172 """Represents an error parsing wildcards in DNS name."""
173
174 def _dnsnamematch(dn, hostname, maxwildcards=1):
175 """Match DNS names according RFC 6125 section 6.4.3.
176
177 This code is effectively copied from CPython's ssl._dnsname_match.
178
179 Returns a bool indicating whether the expected hostname matches
180 the value in ``dn``.
181 """
182 pats = []
183 if not dn:
184 return False
185
186 pieces = dn.split(r'.')
187 leftmost = pieces[0]
188 remainder = pieces[1:]
189 wildcards = leftmost.count('*')
190 if wildcards > maxwildcards:
191 raise wildcarderror(
192 _('too many wildcards in certificate DNS name: %s') % dn)
193
194 # speed up common case w/o wildcards
195 if not wildcards:
196 return dn.lower() == hostname.lower()
197
198 # RFC 6125, section 6.4.3, subitem 1.
199 # The client SHOULD NOT attempt to match a presented identifier in which
200 # the wildcard character comprises a label other than the left-most label.
201 if leftmost == '*':
202 # When '*' is a fragment by itself, it matches a non-empty dotless
203 # fragment.
204 pats.append('[^.]+')
205 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
206 # RFC 6125, section 6.4.3, subitem 3.
207 # The client SHOULD NOT attempt to match a presented identifier
208 # where the wildcard character is embedded within an A-label or
209 # U-label of an internationalized domain name.
210 pats.append(re.escape(leftmost))
211 else:
212 # Otherwise, '*' matches any dotless string, e.g. www*
213 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
214
215 # add the remaining fragments, ignore any wildcards
216 for frag in remainder:
217 pats.append(re.escape(frag))
218
219 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
220 return pat.match(hostname) is not None
221
170 def _verifycert(cert, hostname):
222 def _verifycert(cert, hostname):
171 '''Verify that cert (in socket.getpeercert() format) matches hostname.
223 '''Verify that cert (in socket.getpeercert() format) matches hostname.
172 CRLs is not handled.
224 CRLs is not handled.
@@ -175,33 +227,46 b' def _verifycert(cert, hostname):'
175 '''
227 '''
176 if not cert:
228 if not cert:
177 return _('no certificate received')
229 return _('no certificate received')
178 dnsname = hostname.lower()
179 def matchdnsname(certname):
180 return (certname == dnsname or
181 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
182
230
231 dnsnames = []
183 san = cert.get('subjectAltName', [])
232 san = cert.get('subjectAltName', [])
184 if san:
233 for key, value in san:
185 certnames = [value.lower() for key, value in san if key == 'DNS']
234 if key == 'DNS':
186 for name in certnames:
235 try:
187 if matchdnsname(name):
236 if _dnsnamematch(value, hostname):
188 return None
237 return
189 if certnames:
238 except wildcarderror as e:
190 return _('certificate is for %s') % ', '.join(certnames)
239 return e.message
240
241 dnsnames.append(value)
191
242
192 # subject is only checked when subjectAltName is empty
243 if not dnsnames:
193 for s in cert.get('subject', []):
244 # The subject is only checked when there is no DNS in subjectAltName.
194 key, value = s[0]
245 for sub in cert.get('subject', []):
195 if key == 'commonName':
246 for key, value in sub:
196 try:
247 # According to RFC 2818 the most specific Common Name must
197 # 'subject' entries are unicode
248 # be used.
198 certname = value.lower().encode('ascii')
249 if key == 'commonName':
199 except UnicodeEncodeError:
250 # 'subject' entries are unicide.
200 return _('IDN in certificate not supported')
251 try:
201 if matchdnsname(certname):
252 value = value.encode('ascii')
202 return None
253 except UnicodeEncodeError:
203 return _('certificate is for %s') % certname
254 return _('IDN in certificate not supported')
204 return _('no commonName or subjectAltName found in certificate')
255
256 try:
257 if _dnsnamematch(value, hostname):
258 return
259 except wildcarderror as e:
260 return e.message
261
262 dnsnames.append(value)
263
264 if len(dnsnames) > 1:
265 return _('certificate is for %s') % ', '.join(dnsnames)
266 elif len(dnsnames) == 1:
267 return _('certificate is for %s') % dnsnames[0]
268 else:
269 return _('no commonName or subjectAltName found in certificate')
205
270
206
271
207 # CERT_REQUIRED means fetch the cert from the server all the time AND
272 # CERT_REQUIRED means fetch the cert from the server all the time AND
@@ -51,8 +51,7 b" check(_verifycert(san_cert, 'example.com"
51 # Avoid some pitfalls
51 # Avoid some pitfalls
52 check(_verifycert(cert('*.foo'), 'foo'),
52 check(_verifycert(cert('*.foo'), 'foo'),
53 'certificate is for *.foo')
53 'certificate is for *.foo')
54 check(_verifycert(cert('*o'), 'foo'),
54 check(_verifycert(cert('*o'), 'foo'), None)
55 'certificate is for *o')
56
55
57 check(_verifycert({'subject': ()},
56 check(_verifycert({'subject': ()},
58 'example.com'),
57 'example.com'),
@@ -82,13 +81,12 b" check(_verifycert(cert('*.a.com'), 'a.co"
82 'certificate is for *.a.com')
81 'certificate is for *.a.com')
83 check(_verifycert(cert('*.a.com'), 'Xa.com'),
82 check(_verifycert(cert('*.a.com'), 'Xa.com'),
84 'certificate is for *.a.com')
83 'certificate is for *.a.com')
85 check(_verifycert(cert('*.a.com'), '.a.com'), None)
84 check(_verifycert(cert('*.a.com'), '.a.com'),
85 'certificate is for *.a.com')
86
86
87 # only match one left-most wildcard
87 # only match one left-most wildcard
88 check(_verifycert(cert('f*.com'), 'foo.com'),
88 check(_verifycert(cert('f*.com'), 'foo.com'), None)
89 'certificate is for f*.com')
89 check(_verifycert(cert('f*.com'), 'f.com'), None)
90 check(_verifycert(cert('f*.com'), 'f.com'),
91 'certificate is for f*.com')
92 check(_verifycert(cert('f*.com'), 'bar.com'),
90 check(_verifycert(cert('f*.com'), 'bar.com'),
93 'certificate is for f*.com')
91 'certificate is for f*.com')
94 check(_verifycert(cert('f*.com'), 'foo.a.com'),
92 check(_verifycert(cert('f*.com'), 'foo.a.com'),
@@ -136,10 +134,10 b" check(_verifycert(cert('xn--p*.python.or"
136 idna = u'www*.pythön.org'.encode('idna').decode('ascii')
134 idna = u'www*.pythön.org'.encode('idna').decode('ascii')
137 check(_verifycert(cert(idna),
135 check(_verifycert(cert(idna),
138 u'www.pythön.org'.encode('idna').decode('ascii')),
136 u'www.pythön.org'.encode('idna').decode('ascii')),
139 'certificate is for www*.xn--pythn-mua.org')
137 None)
140 check(_verifycert(cert(idna),
138 check(_verifycert(cert(idna),
141 u'www1.pythön.org'.encode('idna').decode('ascii')),
139 u'www1.pythön.org'.encode('idna').decode('ascii')),
142 'certificate is for www*.xn--pythn-mua.org')
140 None)
143 check(_verifycert(cert(idna),
141 check(_verifycert(cert(idna),
144 u'ftp.pythön.org'.encode('idna').decode('ascii')),
142 u'ftp.pythön.org'.encode('idna').decode('ascii')),
145 'certificate is for www*.xn--pythn-mua.org')
143 'certificate is for www*.xn--pythn-mua.org')
@@ -229,11 +227,12 b" check(_verifycert({}, 'example.com'), 'n"
229 # avoid denials of service by refusing more than one
227 # avoid denials of service by refusing more than one
230 # wildcard per fragment.
228 # wildcard per fragment.
231 check(_verifycert({'subject': (((u'commonName', u'a*b.com'),),)},
229 check(_verifycert({'subject': (((u'commonName', u'a*b.com'),),)},
232 'axxb.com'), 'certificate is for a*b.com')
230 'axxb.com'), None)
233 check(_verifycert({'subject': (((u'commonName', u'a*b.co*'),),)},
231 check(_verifycert({'subject': (((u'commonName', u'a*b.co*'),),)},
234 'axxb.com'), 'certificate is for a*b.co*')
232 'axxb.com'), 'certificate is for a*b.co*')
235 check(_verifycert({'subject': (((u'commonName', u'a*b*.com'),),)},
233 check(_verifycert({'subject': (((u'commonName', u'a*b*.com'),),)},
236 'axxbxxc.com'), 'certificate is for a*b*.com')
234 'axxbxxc.com'),
235 'too many wildcards in certificate DNS name: a*b*.com')
237
236
238 def test_url():
237 def test_url():
239 """
238 """
General Comments 0
You need to be logged in to leave comments. Login now