165 # - see http://bugs.python.org/issue13721 |
166 # - see http://bugs.python.org/issue13721 |
166 if not sslsocket.cipher(): |
167 if not sslsocket.cipher(): |
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. |
173 |
225 |
174 Returns error message if any problems are found and None on success. |
226 Returns error message if any problems are found and None on success. |
175 ''' |
227 ''' |
176 if not cert: |
228 if not cert: |
177 return _('no certificate received') |
229 return _('no certificate received') |
178 dnsname = hostname.lower() |
230 |
179 def matchdnsname(certname): |
231 dnsnames = [] |
180 return (certname == dnsname or |
|
181 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]) |
|
182 |
|
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: |
|
187 if matchdnsname(name): |
|
188 return None |
|
189 if certnames: |
|
190 return _('certificate is for %s') % ', '.join(certnames) |
|
191 |
|
192 # subject is only checked when subjectAltName is empty |
|
193 for s in cert.get('subject', []): |
|
194 key, value = s[0] |
|
195 if key == 'commonName': |
|
196 try: |
235 try: |
197 # 'subject' entries are unicode |
236 if _dnsnamematch(value, hostname): |
198 certname = value.lower().encode('ascii') |
237 return |
199 except UnicodeEncodeError: |
238 except wildcarderror as e: |
200 return _('IDN in certificate not supported') |
239 return e.message |
201 if matchdnsname(certname): |
240 |
202 return None |
241 dnsnames.append(value) |
203 return _('certificate is for %s') % certname |
242 |
204 return _('no commonName or subjectAltName found in certificate') |
243 if not dnsnames: |
|
244 # The subject is only checked when there is no DNS in subjectAltName. |
|
245 for sub in cert.get('subject', []): |
|
246 for key, value in sub: |
|
247 # According to RFC 2818 the most specific Common Name must |
|
248 # be used. |
|
249 if key == 'commonName': |
|
250 # 'subject' entries are unicide. |
|
251 try: |
|
252 value = value.encode('ascii') |
|
253 except UnicodeEncodeError: |
|
254 return _('IDN in certificate not supported') |
|
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 |
208 # validate it against the CA store provided in web.cacerts. |
273 # validate it against the CA store provided in web.cacerts. |
209 |
274 |