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