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