##// END OF EJS Templates
sslutil: remove "strict" argument from validatesocket()...
Gregory Szorc -
r29286:a05a91a3 default
parent child Browse files
Show More
@@ -1,407 +1,400 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import os
13 13 import ssl
14 14 import sys
15 15
16 16 from .i18n import _
17 17 from . import (
18 18 error,
19 19 util,
20 20 )
21 21
22 22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
23 23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
24 24 # all exposed via the "ssl" module.
25 25 #
26 26 # Depending on the version of Python being used, SSL/TLS support is either
27 27 # modern/secure or legacy/insecure. Many operations in this module have
28 28 # separate code paths depending on support in Python.
29 29
30 30 hassni = getattr(ssl, 'HAS_SNI', False)
31 31
32 32 try:
33 33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
34 34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
35 35 except AttributeError:
36 36 OP_NO_SSLv2 = 0x1000000
37 37 OP_NO_SSLv3 = 0x2000000
38 38
39 39 try:
40 40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
41 41 # SSL/TLS features are available.
42 42 SSLContext = ssl.SSLContext
43 43 modernssl = True
44 44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
45 45 except AttributeError:
46 46 modernssl = False
47 47 _canloaddefaultcerts = False
48 48
49 49 # We implement SSLContext using the interface from the standard library.
50 50 class SSLContext(object):
51 51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
52 52 _supportsciphers = sys.version_info >= (2, 7)
53 53
54 54 def __init__(self, protocol):
55 55 # From the public interface of SSLContext
56 56 self.protocol = protocol
57 57 self.check_hostname = False
58 58 self.options = 0
59 59 self.verify_mode = ssl.CERT_NONE
60 60
61 61 # Used by our implementation.
62 62 self._certfile = None
63 63 self._keyfile = None
64 64 self._certpassword = None
65 65 self._cacerts = None
66 66 self._ciphers = None
67 67
68 68 def load_cert_chain(self, certfile, keyfile=None, password=None):
69 69 self._certfile = certfile
70 70 self._keyfile = keyfile
71 71 self._certpassword = password
72 72
73 73 def load_default_certs(self, purpose=None):
74 74 pass
75 75
76 76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
77 77 if capath:
78 78 raise error.Abort('capath not supported')
79 79 if cadata:
80 80 raise error.Abort('cadata not supported')
81 81
82 82 self._cacerts = cafile
83 83
84 84 def set_ciphers(self, ciphers):
85 85 if not self._supportsciphers:
86 86 raise error.Abort('setting ciphers not supported')
87 87
88 88 self._ciphers = ciphers
89 89
90 90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
91 91 # server_hostname is unique to SSLContext.wrap_socket and is used
92 92 # for SNI in that context. So there's nothing for us to do with it
93 93 # in this legacy code since we don't support SNI.
94 94
95 95 args = {
96 96 'keyfile': self._keyfile,
97 97 'certfile': self._certfile,
98 98 'server_side': server_side,
99 99 'cert_reqs': self.verify_mode,
100 100 'ssl_version': self.protocol,
101 101 'ca_certs': self._cacerts,
102 102 }
103 103
104 104 if self._supportsciphers:
105 105 args['ciphers'] = self._ciphers
106 106
107 107 return ssl.wrap_socket(socket, **args)
108 108
109 109 def _hostsettings(ui, hostname):
110 110 """Obtain security settings for a hostname.
111 111
112 112 Returns a dict of settings relevant to that hostname.
113 113 """
114 114 s = {
115 115 # List of 2-tuple of (hash algorithm, hash).
116 116 'certfingerprints': [],
117 117 # Path to file containing concatenated CA certs. Used by
118 118 # SSLContext.load_verify_locations().
119 119 'cafile': None,
120 120 # Whether the legacy [hostfingerprints] section has data for this host.
121 121 'legacyfingerprint': False,
122 122 # ssl.CERT_* constant used by SSLContext.verify_mode.
123 123 'verifymode': None,
124 124 }
125 125
126 126 # Look for fingerprints in [hostsecurity] section. Value is a list
127 127 # of <alg>:<fingerprint> strings.
128 128 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
129 129 [])
130 130 for fingerprint in fingerprints:
131 131 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
132 132 raise error.Abort(_('invalid fingerprint for %s: %s') % (
133 133 hostname, fingerprint),
134 134 hint=_('must begin with "sha1:", "sha256:", '
135 135 'or "sha512:"'))
136 136
137 137 alg, fingerprint = fingerprint.split(':', 1)
138 138 fingerprint = fingerprint.replace(':', '').lower()
139 139 s['certfingerprints'].append((alg, fingerprint))
140 140
141 141 # Fingerprints from [hostfingerprints] are always SHA-1.
142 142 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
143 143 fingerprint = fingerprint.replace(':', '').lower()
144 144 s['certfingerprints'].append(('sha1', fingerprint))
145 145 s['legacyfingerprint'] = True
146 146
147 147 # If a host cert fingerprint is defined, it is the only thing that
148 148 # matters. No need to validate CA certs.
149 149 if s['certfingerprints']:
150 150 s['verifymode'] = ssl.CERT_NONE
151 151
152 152 # If --insecure is used, don't take CAs into consideration.
153 153 elif ui.insecureconnections:
154 154 s['verifymode'] = ssl.CERT_NONE
155 155
156 156 # Try to hook up CA certificate validation unless something above
157 157 # makes it not necessary.
158 158 if s['verifymode'] is None:
159 159 # Find global certificates file in config.
160 160 cafile = ui.config('web', 'cacerts')
161 161
162 162 if cafile:
163 163 cafile = util.expandpath(cafile)
164 164 if not os.path.exists(cafile):
165 165 raise error.Abort(_('could not find web.cacerts: %s') % cafile)
166 166 else:
167 167 # No global CA certs. See if we can load defaults.
168 168 cafile = _defaultcacerts()
169 169 if cafile:
170 170 ui.debug('using %s to enable OS X system CA\n' % cafile)
171 171
172 172 s['cafile'] = cafile
173 173
174 174 # Require certificate validation if CA certs are being loaded and
175 175 # verification hasn't been disabled above.
176 176 if cafile or _canloaddefaultcerts:
177 177 s['verifymode'] = ssl.CERT_REQUIRED
178 178 else:
179 179 # At this point we don't have a fingerprint, aren't being
180 180 # explicitly insecure, and can't load CA certs. Connecting
181 181 # at this point is insecure. But we do it for BC reasons.
182 182 # TODO abort here to make secure by default.
183 183 s['verifymode'] = ssl.CERT_NONE
184 184
185 185 assert s['verifymode'] is not None
186 186
187 187 return s
188 188
189 189 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
190 190 """Add SSL/TLS to a socket.
191 191
192 192 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
193 193 choices based on what security options are available.
194 194
195 195 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
196 196 the following additional arguments:
197 197
198 198 * serverhostname - The expected hostname of the remote server. If the
199 199 server (and client) support SNI, this tells the server which certificate
200 200 to use.
201 201 """
202 202 if not serverhostname:
203 203 raise error.Abort('serverhostname argument is required')
204 204
205 205 settings = _hostsettings(ui, serverhostname)
206 206
207 207 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
208 208 # that both ends support, including TLS protocols. On legacy stacks,
209 209 # the highest it likely goes in TLS 1.0. On modern stacks, it can
210 210 # support TLS 1.2.
211 211 #
212 212 # The PROTOCOL_TLSv* constants select a specific TLS version
213 213 # only (as opposed to multiple versions). So the method for
214 214 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
215 215 # disable protocols via SSLContext.options and OP_NO_* constants.
216 216 # However, SSLContext.options doesn't work unless we have the
217 217 # full/real SSLContext available to us.
218 218 #
219 219 # SSLv2 and SSLv3 are broken. We ban them outright.
220 220 if modernssl:
221 221 protocol = ssl.PROTOCOL_SSLv23
222 222 else:
223 223 protocol = ssl.PROTOCOL_TLSv1
224 224
225 225 # TODO use ssl.create_default_context() on modernssl.
226 226 sslcontext = SSLContext(protocol)
227 227
228 228 # This is a no-op on old Python.
229 229 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
230 230
231 231 # This still works on our fake SSLContext.
232 232 sslcontext.verify_mode = settings['verifymode']
233 233
234 234 if certfile is not None:
235 235 def password():
236 236 f = keyfile or certfile
237 237 return ui.getpass(_('passphrase for %s: ') % f, '')
238 238 sslcontext.load_cert_chain(certfile, keyfile, password)
239 239
240 240 if settings['cafile'] is not None:
241 241 sslcontext.load_verify_locations(cafile=settings['cafile'])
242 242 caloaded = True
243 243 else:
244 244 # This is a no-op on old Python.
245 245 sslcontext.load_default_certs()
246 246 caloaded = _canloaddefaultcerts
247 247
248 248 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
249 249 # check if wrap_socket failed silently because socket had been
250 250 # closed
251 251 # - see http://bugs.python.org/issue13721
252 252 if not sslsocket.cipher():
253 253 raise error.Abort(_('ssl connection failed'))
254 254
255 255 sslsocket._hgstate = {
256 256 'caloaded': caloaded,
257 257 'hostname': serverhostname,
258 258 'settings': settings,
259 259 'ui': ui,
260 260 }
261 261
262 262 return sslsocket
263 263
264 264 def _verifycert(cert, hostname):
265 265 '''Verify that cert (in socket.getpeercert() format) matches hostname.
266 266 CRLs is not handled.
267 267
268 268 Returns error message if any problems are found and None on success.
269 269 '''
270 270 if not cert:
271 271 return _('no certificate received')
272 272 dnsname = hostname.lower()
273 273 def matchdnsname(certname):
274 274 return (certname == dnsname or
275 275 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
276 276
277 277 san = cert.get('subjectAltName', [])
278 278 if san:
279 279 certnames = [value.lower() for key, value in san if key == 'DNS']
280 280 for name in certnames:
281 281 if matchdnsname(name):
282 282 return None
283 283 if certnames:
284 284 return _('certificate is for %s') % ', '.join(certnames)
285 285
286 286 # subject is only checked when subjectAltName is empty
287 287 for s in cert.get('subject', []):
288 288 key, value = s[0]
289 289 if key == 'commonName':
290 290 try:
291 291 # 'subject' entries are unicode
292 292 certname = value.lower().encode('ascii')
293 293 except UnicodeEncodeError:
294 294 return _('IDN in certificate not supported')
295 295 if matchdnsname(certname):
296 296 return None
297 297 return _('certificate is for %s') % certname
298 298 return _('no commonName or subjectAltName found in certificate')
299 299
300 300
301 301 # CERT_REQUIRED means fetch the cert from the server all the time AND
302 302 # validate it against the CA store provided in web.cacerts.
303 303
304 304 def _plainapplepython():
305 305 """return true if this seems to be a pure Apple Python that
306 306 * is unfrozen and presumably has the whole mercurial module in the file
307 307 system
308 308 * presumably is an Apple Python that uses Apple OpenSSL which has patches
309 309 for using system certificate store CAs in addition to the provided
310 310 cacerts file
311 311 """
312 312 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
313 313 return False
314 314 exe = os.path.realpath(sys.executable).lower()
315 315 return (exe.startswith('/usr/bin/python') or
316 316 exe.startswith('/system/library/frameworks/python.framework/'))
317 317
318 318 def _defaultcacerts():
319 319 """return path to default CA certificates or None."""
320 320 if _plainapplepython():
321 321 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
322 322 if os.path.exists(dummycert):
323 323 return dummycert
324 324
325 325 return None
326 326
327 def validatesocket(sock, strict=False):
327 def validatesocket(sock):
328 328 """Validate a socket meets security requiremnets.
329 329
330 330 The passed socket must have been created with ``wrapsocket()``.
331 331 """
332 332 host = sock._hgstate['hostname']
333 333 ui = sock._hgstate['ui']
334 334 settings = sock._hgstate['settings']
335 335
336 336 try:
337 337 peercert = sock.getpeercert(True)
338 338 peercert2 = sock.getpeercert()
339 339 except AttributeError:
340 340 raise error.Abort(_('%s ssl connection error') % host)
341 341
342 342 if not peercert:
343 343 raise error.Abort(_('%s certificate error: '
344 344 'no certificate received') % host)
345 345
346 346 # If a certificate fingerprint is pinned, use it and only it to
347 347 # validate the remote cert.
348 348 peerfingerprints = {
349 349 'sha1': util.sha1(peercert).hexdigest(),
350 350 'sha256': util.sha256(peercert).hexdigest(),
351 351 'sha512': util.sha512(peercert).hexdigest(),
352 352 }
353 353 nicefingerprint = ':'.join([peerfingerprints['sha1'][x:x + 2]
354 354 for x in range(0, len(peerfingerprints['sha1']), 2)])
355 355
356 356 if settings['legacyfingerprint']:
357 357 section = 'hostfingerprint'
358 358 else:
359 359 section = 'hostsecurity'
360 360
361 361 if settings['certfingerprints']:
362 362 fingerprintmatch = False
363 363 for hash, fingerprint in settings['certfingerprints']:
364 364 if peerfingerprints[hash].lower() == fingerprint:
365 365 fingerprintmatch = True
366 366 break
367 367 if not fingerprintmatch:
368 368 raise error.Abort(_('certificate for %s has unexpected '
369 369 'fingerprint %s') % (host, nicefingerprint),
370 370 hint=_('check %s configuration') % section)
371 371 ui.debug('%s certificate matched fingerprint %s\n' %
372 372 (host, nicefingerprint))
373 373 return
374 374
375 375 # If insecure connections were explicitly requested via --insecure,
376 376 # print a warning and do no verification.
377 377 #
378 378 # It may seem odd that this is checked *after* host fingerprint pinning.
379 379 # This is for backwards compatibility (for now). The message is also
380 380 # the same as below for BC.
381 381 if ui.insecureconnections:
382 382 ui.warn(_('warning: %s certificate with fingerprint %s not '
383 383 'verified (check %s or web.cacerts '
384 384 'config setting)\n') %
385 385 (host, nicefingerprint, section))
386 386 return
387 387
388 388 if not sock._hgstate['caloaded']:
389 if strict:
390 raise error.Abort(_('%s certificate with fingerprint %s not '
391 'verified') % (host, nicefingerprint),
392 hint=_('check %s or web.cacerts config '
393 'setting') % section)
394 else:
395 ui.warn(_('warning: %s certificate with fingerprint %s '
396 'not verified (check %s or web.cacerts config '
397 'setting)\n') %
398 (host, nicefingerprint, section))
399
389 ui.warn(_('warning: %s certificate with fingerprint %s '
390 'not verified (check %s or web.cacerts config '
391 'setting)\n') %
392 (host, nicefingerprint, section))
400 393 return
401 394
402 395 msg = _verifycert(peercert2, host)
403 396 if msg:
404 397 raise error.Abort(_('%s certificate error: %s') % (host, msg),
405 398 hint=_('configure %s %s or use '
406 399 '--insecure to connect insecurely') %
407 400 (section, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now