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