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