##// END OF EJS Templates
sslutil: refactor code for fingerprint matching...
Gregory Szorc -
r29291:15e533b7 default
parent child Browse files
Show More
@@ -1,414 +1,411 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 # Whether we should attempt to load default/available CA certs
116 116 # if an explicit ``cafile`` is not defined.
117 117 'allowloaddefaultcerts': True,
118 118 # List of 2-tuple of (hash algorithm, hash).
119 119 'certfingerprints': [],
120 120 # Path to file containing concatenated CA certs. Used by
121 121 # SSLContext.load_verify_locations().
122 122 'cafile': None,
123 123 # Whether certificate verification should be disabled.
124 124 'disablecertverification': False,
125 125 # Whether the legacy [hostfingerprints] section has data for this host.
126 126 'legacyfingerprint': False,
127 127 # ssl.CERT_* constant used by SSLContext.verify_mode.
128 128 'verifymode': None,
129 129 }
130 130
131 131 # Look for fingerprints in [hostsecurity] section. Value is a list
132 132 # of <alg>:<fingerprint> strings.
133 133 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
134 134 [])
135 135 for fingerprint in fingerprints:
136 136 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
137 137 raise error.Abort(_('invalid fingerprint for %s: %s') % (
138 138 hostname, fingerprint),
139 139 hint=_('must begin with "sha1:", "sha256:", '
140 140 'or "sha512:"'))
141 141
142 142 alg, fingerprint = fingerprint.split(':', 1)
143 143 fingerprint = fingerprint.replace(':', '').lower()
144 144 s['certfingerprints'].append((alg, fingerprint))
145 145
146 146 # Fingerprints from [hostfingerprints] are always SHA-1.
147 147 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
148 148 fingerprint = fingerprint.replace(':', '').lower()
149 149 s['certfingerprints'].append(('sha1', fingerprint))
150 150 s['legacyfingerprint'] = True
151 151
152 152 # If a host cert fingerprint is defined, it is the only thing that
153 153 # matters. No need to validate CA certs.
154 154 if s['certfingerprints']:
155 155 s['verifymode'] = ssl.CERT_NONE
156 156
157 157 # If --insecure is used, don't take CAs into consideration.
158 158 elif ui.insecureconnections:
159 159 s['disablecertverification'] = True
160 160 s['verifymode'] = ssl.CERT_NONE
161 161
162 162 if ui.configbool('devel', 'disableloaddefaultcerts'):
163 163 s['allowloaddefaultcerts'] = False
164 164
165 165 # Try to hook up CA certificate validation unless something above
166 166 # makes it not necessary.
167 167 if s['verifymode'] is None:
168 168 # Find global certificates file in config.
169 169 cafile = ui.config('web', 'cacerts')
170 170
171 171 if cafile:
172 172 cafile = util.expandpath(cafile)
173 173 if not os.path.exists(cafile):
174 174 raise error.Abort(_('could not find web.cacerts: %s') % cafile)
175 175 else:
176 176 # No global CA certs. See if we can load defaults.
177 177 cafile = _defaultcacerts()
178 178 if cafile:
179 179 ui.debug('using %s to enable OS X system CA\n' % cafile)
180 180
181 181 s['cafile'] = cafile
182 182
183 183 # Require certificate validation if CA certs are being loaded and
184 184 # verification hasn't been disabled above.
185 185 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
186 186 s['verifymode'] = ssl.CERT_REQUIRED
187 187 else:
188 188 # At this point we don't have a fingerprint, aren't being
189 189 # explicitly insecure, and can't load CA certs. Connecting
190 190 # at this point is insecure. But we do it for BC reasons.
191 191 # TODO abort here to make secure by default.
192 192 s['verifymode'] = ssl.CERT_NONE
193 193
194 194 assert s['verifymode'] is not None
195 195
196 196 return s
197 197
198 198 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
199 199 """Add SSL/TLS to a socket.
200 200
201 201 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
202 202 choices based on what security options are available.
203 203
204 204 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
205 205 the following additional arguments:
206 206
207 207 * serverhostname - The expected hostname of the remote server. If the
208 208 server (and client) support SNI, this tells the server which certificate
209 209 to use.
210 210 """
211 211 if not serverhostname:
212 212 raise error.Abort('serverhostname argument is required')
213 213
214 214 settings = _hostsettings(ui, serverhostname)
215 215
216 216 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
217 217 # that both ends support, including TLS protocols. On legacy stacks,
218 218 # the highest it likely goes in TLS 1.0. On modern stacks, it can
219 219 # support TLS 1.2.
220 220 #
221 221 # The PROTOCOL_TLSv* constants select a specific TLS version
222 222 # only (as opposed to multiple versions). So the method for
223 223 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
224 224 # disable protocols via SSLContext.options and OP_NO_* constants.
225 225 # However, SSLContext.options doesn't work unless we have the
226 226 # full/real SSLContext available to us.
227 227 #
228 228 # SSLv2 and SSLv3 are broken. We ban them outright.
229 229 if modernssl:
230 230 protocol = ssl.PROTOCOL_SSLv23
231 231 else:
232 232 protocol = ssl.PROTOCOL_TLSv1
233 233
234 234 # TODO use ssl.create_default_context() on modernssl.
235 235 sslcontext = SSLContext(protocol)
236 236
237 237 # This is a no-op on old Python.
238 238 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
239 239
240 240 # This still works on our fake SSLContext.
241 241 sslcontext.verify_mode = settings['verifymode']
242 242
243 243 if certfile is not None:
244 244 def password():
245 245 f = keyfile or certfile
246 246 return ui.getpass(_('passphrase for %s: ') % f, '')
247 247 sslcontext.load_cert_chain(certfile, keyfile, password)
248 248
249 249 if settings['cafile'] is not None:
250 250 sslcontext.load_verify_locations(cafile=settings['cafile'])
251 251 caloaded = True
252 252 elif settings['allowloaddefaultcerts']:
253 253 # This is a no-op on old Python.
254 254 sslcontext.load_default_certs()
255 255 caloaded = True
256 256 else:
257 257 caloaded = False
258 258
259 259 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
260 260 # check if wrap_socket failed silently because socket had been
261 261 # closed
262 262 # - see http://bugs.python.org/issue13721
263 263 if not sslsocket.cipher():
264 264 raise error.Abort(_('ssl connection failed'))
265 265
266 266 sslsocket._hgstate = {
267 267 'caloaded': caloaded,
268 268 'hostname': serverhostname,
269 269 'settings': settings,
270 270 'ui': ui,
271 271 }
272 272
273 273 return sslsocket
274 274
275 275 def _verifycert(cert, hostname):
276 276 '''Verify that cert (in socket.getpeercert() format) matches hostname.
277 277 CRLs is not handled.
278 278
279 279 Returns error message if any problems are found and None on success.
280 280 '''
281 281 if not cert:
282 282 return _('no certificate received')
283 283 dnsname = hostname.lower()
284 284 def matchdnsname(certname):
285 285 return (certname == dnsname or
286 286 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
287 287
288 288 san = cert.get('subjectAltName', [])
289 289 if san:
290 290 certnames = [value.lower() for key, value in san if key == 'DNS']
291 291 for name in certnames:
292 292 if matchdnsname(name):
293 293 return None
294 294 if certnames:
295 295 return _('certificate is for %s') % ', '.join(certnames)
296 296
297 297 # subject is only checked when subjectAltName is empty
298 298 for s in cert.get('subject', []):
299 299 key, value = s[0]
300 300 if key == 'commonName':
301 301 try:
302 302 # 'subject' entries are unicode
303 303 certname = value.lower().encode('ascii')
304 304 except UnicodeEncodeError:
305 305 return _('IDN in certificate not supported')
306 306 if matchdnsname(certname):
307 307 return None
308 308 return _('certificate is for %s') % certname
309 309 return _('no commonName or subjectAltName found in certificate')
310 310
311 311
312 312 # CERT_REQUIRED means fetch the cert from the server all the time AND
313 313 # validate it against the CA store provided in web.cacerts.
314 314
315 315 def _plainapplepython():
316 316 """return true if this seems to be a pure Apple Python that
317 317 * is unfrozen and presumably has the whole mercurial module in the file
318 318 system
319 319 * presumably is an Apple Python that uses Apple OpenSSL which has patches
320 320 for using system certificate store CAs in addition to the provided
321 321 cacerts file
322 322 """
323 323 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
324 324 return False
325 325 exe = os.path.realpath(sys.executable).lower()
326 326 return (exe.startswith('/usr/bin/python') or
327 327 exe.startswith('/system/library/frameworks/python.framework/'))
328 328
329 329 def _defaultcacerts():
330 330 """return path to default CA certificates or None."""
331 331 if _plainapplepython():
332 332 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
333 333 if os.path.exists(dummycert):
334 334 return dummycert
335 335
336 336 return None
337 337
338 338 def validatesocket(sock):
339 339 """Validate a socket meets security requiremnets.
340 340
341 341 The passed socket must have been created with ``wrapsocket()``.
342 342 """
343 343 host = sock._hgstate['hostname']
344 344 ui = sock._hgstate['ui']
345 345 settings = sock._hgstate['settings']
346 346
347 347 try:
348 348 peercert = sock.getpeercert(True)
349 349 peercert2 = sock.getpeercert()
350 350 except AttributeError:
351 351 raise error.Abort(_('%s ssl connection error') % host)
352 352
353 353 if not peercert:
354 354 raise error.Abort(_('%s certificate error: '
355 355 'no certificate received') % host)
356 356
357 357 if settings['disablecertverification']:
358 358 # We don't print the certificate fingerprint because it shouldn't
359 359 # be necessary: if the user requested certificate verification be
360 360 # disabled, they presumably already saw a message about the inability
361 361 # to verify the certificate and this message would have printed the
362 362 # fingerprint. So printing the fingerprint here adds little to no
363 363 # value.
364 364 ui.warn(_('warning: connection security to %s is disabled per current '
365 365 'settings; communication is susceptible to eavesdropping '
366 366 'and tampering\n') % host)
367 367 return
368 368
369 369 # If a certificate fingerprint is pinned, use it and only it to
370 370 # validate the remote cert.
371 371 peerfingerprints = {
372 372 'sha1': util.sha1(peercert).hexdigest(),
373 373 'sha256': util.sha256(peercert).hexdigest(),
374 374 'sha512': util.sha512(peercert).hexdigest(),
375 375 }
376 376
377 377 def fmtfingerprint(s):
378 378 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
379 379
380 380 legacyfingerprint = fmtfingerprint(peerfingerprints['sha1'])
381 381 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
382 382
383 383 if settings['legacyfingerprint']:
384 384 section = 'hostfingerprint'
385 385 else:
386 386 section = 'hostsecurity'
387 387
388 388 if settings['certfingerprints']:
389 fingerprintmatch = False
390 389 for hash, fingerprint in settings['certfingerprints']:
391 390 if peerfingerprints[hash].lower() == fingerprint:
392 fingerprintmatch = True
393 break
394 if not fingerprintmatch:
395 raise error.Abort(_('certificate for %s has unexpected '
396 'fingerprint %s') % (host, legacyfingerprint),
397 hint=_('check %s configuration') % section)
398 ui.debug('%s certificate matched fingerprint %s\n' %
399 (host, legacyfingerprint))
400 return
391 ui.debug('%s certificate matched fingerprint %s:%s\n' %
392 (host, hash, fmtfingerprint(fingerprint)))
393 return
394
395 raise error.Abort(_('certificate for %s has unexpected '
396 'fingerprint %s') % (host, legacyfingerprint),
397 hint=_('check %s configuration') % section)
401 398
402 399 if not sock._hgstate['caloaded']:
403 400 ui.warn(_('warning: %s certificate with fingerprint %s '
404 401 'not verified (check %s or web.cacerts config '
405 402 'setting)\n') %
406 403 (host, nicefingerprint, section))
407 404 return
408 405
409 406 msg = _verifycert(peercert2, host)
410 407 if msg:
411 408 raise error.Abort(_('%s certificate error: %s') % (host, msg),
412 409 hint=_('configure %s %s or use '
413 410 '--insecure to connect insecurely') %
414 411 (section, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now