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