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