##// END OF EJS Templates
sslutil: implement wrapserversocket()...
Gregory Szorc -
r29554:4a7b0c69 default
parent child Browse files
Show More
@@ -1,610 +1,656 b''
1 # sslutil.py - SSL handling for mercurial
1 # sslutil.py - SSL handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import re
14 import re
15 import ssl
15 import ssl
16 import sys
16 import sys
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 error,
20 error,
21 util,
21 util,
22 )
22 )
23
23
24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
26 # all exposed via the "ssl" module.
26 # all exposed via the "ssl" module.
27 #
27 #
28 # Depending on the version of Python being used, SSL/TLS support is either
28 # Depending on the version of Python being used, SSL/TLS support is either
29 # modern/secure or legacy/insecure. Many operations in this module have
29 # modern/secure or legacy/insecure. Many operations in this module have
30 # separate code paths depending on support in Python.
30 # separate code paths depending on support in Python.
31
31
32 hassni = getattr(ssl, 'HAS_SNI', False)
32 hassni = getattr(ssl, 'HAS_SNI', False)
33
33
34 try:
34 try:
35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
37 except AttributeError:
37 except AttributeError:
38 OP_NO_SSLv2 = 0x1000000
38 OP_NO_SSLv2 = 0x1000000
39 OP_NO_SSLv3 = 0x2000000
39 OP_NO_SSLv3 = 0x2000000
40
40
41 try:
41 try:
42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
43 # SSL/TLS features are available.
43 # SSL/TLS features are available.
44 SSLContext = ssl.SSLContext
44 SSLContext = ssl.SSLContext
45 modernssl = True
45 modernssl = True
46 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
46 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
47 except AttributeError:
47 except AttributeError:
48 modernssl = False
48 modernssl = False
49 _canloaddefaultcerts = False
49 _canloaddefaultcerts = False
50
50
51 # We implement SSLContext using the interface from the standard library.
51 # We implement SSLContext using the interface from the standard library.
52 class SSLContext(object):
52 class SSLContext(object):
53 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
53 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
54 _supportsciphers = sys.version_info >= (2, 7)
54 _supportsciphers = sys.version_info >= (2, 7)
55
55
56 def __init__(self, protocol):
56 def __init__(self, protocol):
57 # From the public interface of SSLContext
57 # From the public interface of SSLContext
58 self.protocol = protocol
58 self.protocol = protocol
59 self.check_hostname = False
59 self.check_hostname = False
60 self.options = 0
60 self.options = 0
61 self.verify_mode = ssl.CERT_NONE
61 self.verify_mode = ssl.CERT_NONE
62
62
63 # Used by our implementation.
63 # Used by our implementation.
64 self._certfile = None
64 self._certfile = None
65 self._keyfile = None
65 self._keyfile = None
66 self._certpassword = None
66 self._certpassword = None
67 self._cacerts = None
67 self._cacerts = None
68 self._ciphers = None
68 self._ciphers = None
69
69
70 def load_cert_chain(self, certfile, keyfile=None, password=None):
70 def load_cert_chain(self, certfile, keyfile=None, password=None):
71 self._certfile = certfile
71 self._certfile = certfile
72 self._keyfile = keyfile
72 self._keyfile = keyfile
73 self._certpassword = password
73 self._certpassword = password
74
74
75 def load_default_certs(self, purpose=None):
75 def load_default_certs(self, purpose=None):
76 pass
76 pass
77
77
78 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
79 if capath:
79 if capath:
80 raise error.Abort(_('capath not supported'))
80 raise error.Abort(_('capath not supported'))
81 if cadata:
81 if cadata:
82 raise error.Abort(_('cadata not supported'))
82 raise error.Abort(_('cadata not supported'))
83
83
84 self._cacerts = cafile
84 self._cacerts = cafile
85
85
86 def set_ciphers(self, ciphers):
86 def set_ciphers(self, ciphers):
87 if not self._supportsciphers:
87 if not self._supportsciphers:
88 raise error.Abort(_('setting ciphers not supported'))
88 raise error.Abort(_('setting ciphers not supported'))
89
89
90 self._ciphers = ciphers
90 self._ciphers = ciphers
91
91
92 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 def wrap_socket(self, socket, server_hostname=None, server_side=False):
93 # server_hostname is unique to SSLContext.wrap_socket and is used
93 # server_hostname is unique to SSLContext.wrap_socket and is used
94 # for SNI in that context. So there's nothing for us to do with it
94 # for SNI in that context. So there's nothing for us to do with it
95 # in this legacy code since we don't support SNI.
95 # in this legacy code since we don't support SNI.
96
96
97 args = {
97 args = {
98 'keyfile': self._keyfile,
98 'keyfile': self._keyfile,
99 'certfile': self._certfile,
99 'certfile': self._certfile,
100 'server_side': server_side,
100 'server_side': server_side,
101 'cert_reqs': self.verify_mode,
101 'cert_reqs': self.verify_mode,
102 'ssl_version': self.protocol,
102 'ssl_version': self.protocol,
103 'ca_certs': self._cacerts,
103 'ca_certs': self._cacerts,
104 }
104 }
105
105
106 if self._supportsciphers:
106 if self._supportsciphers:
107 args['ciphers'] = self._ciphers
107 args['ciphers'] = self._ciphers
108
108
109 return ssl.wrap_socket(socket, **args)
109 return ssl.wrap_socket(socket, **args)
110
110
111 def _hostsettings(ui, hostname):
111 def _hostsettings(ui, hostname):
112 """Obtain security settings for a hostname.
112 """Obtain security settings for a hostname.
113
113
114 Returns a dict of settings relevant to that hostname.
114 Returns a dict of settings relevant to that hostname.
115 """
115 """
116 s = {
116 s = {
117 # Whether we should attempt to load default/available CA certs
117 # Whether we should attempt to load default/available CA certs
118 # if an explicit ``cafile`` is not defined.
118 # if an explicit ``cafile`` is not defined.
119 'allowloaddefaultcerts': True,
119 'allowloaddefaultcerts': True,
120 # List of 2-tuple of (hash algorithm, hash).
120 # List of 2-tuple of (hash algorithm, hash).
121 'certfingerprints': [],
121 'certfingerprints': [],
122 # Path to file containing concatenated CA certs. Used by
122 # Path to file containing concatenated CA certs. Used by
123 # SSLContext.load_verify_locations().
123 # SSLContext.load_verify_locations().
124 'cafile': None,
124 'cafile': None,
125 # Whether certificate verification should be disabled.
125 # Whether certificate verification should be disabled.
126 'disablecertverification': False,
126 'disablecertverification': False,
127 # Whether the legacy [hostfingerprints] section has data for this host.
127 # Whether the legacy [hostfingerprints] section has data for this host.
128 'legacyfingerprint': False,
128 'legacyfingerprint': False,
129 # PROTOCOL_* constant to use for SSLContext.__init__.
129 # PROTOCOL_* constant to use for SSLContext.__init__.
130 'protocol': None,
130 'protocol': None,
131 # ssl.CERT_* constant used by SSLContext.verify_mode.
131 # ssl.CERT_* constant used by SSLContext.verify_mode.
132 'verifymode': None,
132 'verifymode': None,
133 # Defines extra ssl.OP* bitwise options to set.
133 # Defines extra ssl.OP* bitwise options to set.
134 'ctxoptions': None,
134 'ctxoptions': None,
135 }
135 }
136
136
137 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
137 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
138 # that both ends support, including TLS protocols. On legacy stacks,
138 # that both ends support, including TLS protocols. On legacy stacks,
139 # the highest it likely goes in TLS 1.0. On modern stacks, it can
139 # the highest it likely goes in TLS 1.0. On modern stacks, it can
140 # support TLS 1.2.
140 # support TLS 1.2.
141 #
141 #
142 # The PROTOCOL_TLSv* constants select a specific TLS version
142 # The PROTOCOL_TLSv* constants select a specific TLS version
143 # only (as opposed to multiple versions). So the method for
143 # only (as opposed to multiple versions). So the method for
144 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
144 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
145 # disable protocols via SSLContext.options and OP_NO_* constants.
145 # disable protocols via SSLContext.options and OP_NO_* constants.
146 # However, SSLContext.options doesn't work unless we have the
146 # However, SSLContext.options doesn't work unless we have the
147 # full/real SSLContext available to us.
147 # full/real SSLContext available to us.
148 if modernssl:
148 if modernssl:
149 s['protocol'] = ssl.PROTOCOL_SSLv23
149 s['protocol'] = ssl.PROTOCOL_SSLv23
150 else:
150 else:
151 s['protocol'] = ssl.PROTOCOL_TLSv1
151 s['protocol'] = ssl.PROTOCOL_TLSv1
152
152
153 # SSLv2 and SSLv3 are broken. We ban them outright.
153 # SSLv2 and SSLv3 are broken. We ban them outright.
154 # WARNING: ctxoptions doesn't have an effect unless the modern ssl module
154 # WARNING: ctxoptions doesn't have an effect unless the modern ssl module
155 # is available. Be careful when adding flags!
155 # is available. Be careful when adding flags!
156 s['ctxoptions'] = OP_NO_SSLv2 | OP_NO_SSLv3
156 s['ctxoptions'] = OP_NO_SSLv2 | OP_NO_SSLv3
157
157
158 # Look for fingerprints in [hostsecurity] section. Value is a list
158 # Look for fingerprints in [hostsecurity] section. Value is a list
159 # of <alg>:<fingerprint> strings.
159 # of <alg>:<fingerprint> strings.
160 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
160 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
161 [])
161 [])
162 for fingerprint in fingerprints:
162 for fingerprint in fingerprints:
163 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
163 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
164 raise error.Abort(_('invalid fingerprint for %s: %s') % (
164 raise error.Abort(_('invalid fingerprint for %s: %s') % (
165 hostname, fingerprint),
165 hostname, fingerprint),
166 hint=_('must begin with "sha1:", "sha256:", '
166 hint=_('must begin with "sha1:", "sha256:", '
167 'or "sha512:"'))
167 'or "sha512:"'))
168
168
169 alg, fingerprint = fingerprint.split(':', 1)
169 alg, fingerprint = fingerprint.split(':', 1)
170 fingerprint = fingerprint.replace(':', '').lower()
170 fingerprint = fingerprint.replace(':', '').lower()
171 s['certfingerprints'].append((alg, fingerprint))
171 s['certfingerprints'].append((alg, fingerprint))
172
172
173 # Fingerprints from [hostfingerprints] are always SHA-1.
173 # Fingerprints from [hostfingerprints] are always SHA-1.
174 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
174 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
175 fingerprint = fingerprint.replace(':', '').lower()
175 fingerprint = fingerprint.replace(':', '').lower()
176 s['certfingerprints'].append(('sha1', fingerprint))
176 s['certfingerprints'].append(('sha1', fingerprint))
177 s['legacyfingerprint'] = True
177 s['legacyfingerprint'] = True
178
178
179 # If a host cert fingerprint is defined, it is the only thing that
179 # If a host cert fingerprint is defined, it is the only thing that
180 # matters. No need to validate CA certs.
180 # matters. No need to validate CA certs.
181 if s['certfingerprints']:
181 if s['certfingerprints']:
182 s['verifymode'] = ssl.CERT_NONE
182 s['verifymode'] = ssl.CERT_NONE
183 s['allowloaddefaultcerts'] = False
183 s['allowloaddefaultcerts'] = False
184
184
185 # If --insecure is used, don't take CAs into consideration.
185 # If --insecure is used, don't take CAs into consideration.
186 elif ui.insecureconnections:
186 elif ui.insecureconnections:
187 s['disablecertverification'] = True
187 s['disablecertverification'] = True
188 s['verifymode'] = ssl.CERT_NONE
188 s['verifymode'] = ssl.CERT_NONE
189 s['allowloaddefaultcerts'] = False
189 s['allowloaddefaultcerts'] = False
190
190
191 if ui.configbool('devel', 'disableloaddefaultcerts'):
191 if ui.configbool('devel', 'disableloaddefaultcerts'):
192 s['allowloaddefaultcerts'] = False
192 s['allowloaddefaultcerts'] = False
193
193
194 # If both fingerprints and a per-host ca file are specified, issue a warning
194 # If both fingerprints and a per-host ca file are specified, issue a warning
195 # because users should not be surprised about what security is or isn't
195 # because users should not be surprised about what security is or isn't
196 # being performed.
196 # being performed.
197 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
197 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
198 if s['certfingerprints'] and cafile:
198 if s['certfingerprints'] and cafile:
199 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
199 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
200 'fingerprints defined; using host fingerprints for '
200 'fingerprints defined; using host fingerprints for '
201 'verification)\n') % hostname)
201 'verification)\n') % hostname)
202
202
203 # Try to hook up CA certificate validation unless something above
203 # Try to hook up CA certificate validation unless something above
204 # makes it not necessary.
204 # makes it not necessary.
205 if s['verifymode'] is None:
205 if s['verifymode'] is None:
206 # Look at per-host ca file first.
206 # Look at per-host ca file first.
207 if cafile:
207 if cafile:
208 cafile = util.expandpath(cafile)
208 cafile = util.expandpath(cafile)
209 if not os.path.exists(cafile):
209 if not os.path.exists(cafile):
210 raise error.Abort(_('path specified by %s does not exist: %s') %
210 raise error.Abort(_('path specified by %s does not exist: %s') %
211 ('hostsecurity.%s:verifycertsfile' % hostname,
211 ('hostsecurity.%s:verifycertsfile' % hostname,
212 cafile))
212 cafile))
213 s['cafile'] = cafile
213 s['cafile'] = cafile
214 else:
214 else:
215 # Find global certificates file in config.
215 # Find global certificates file in config.
216 cafile = ui.config('web', 'cacerts')
216 cafile = ui.config('web', 'cacerts')
217
217
218 if cafile:
218 if cafile:
219 cafile = util.expandpath(cafile)
219 cafile = util.expandpath(cafile)
220 if not os.path.exists(cafile):
220 if not os.path.exists(cafile):
221 raise error.Abort(_('could not find web.cacerts: %s') %
221 raise error.Abort(_('could not find web.cacerts: %s') %
222 cafile)
222 cafile)
223 elif s['allowloaddefaultcerts']:
223 elif s['allowloaddefaultcerts']:
224 # CAs not defined in config. Try to find system bundles.
224 # CAs not defined in config. Try to find system bundles.
225 cafile = _defaultcacerts(ui)
225 cafile = _defaultcacerts(ui)
226 if cafile:
226 if cafile:
227 ui.debug('using %s for CA file\n' % cafile)
227 ui.debug('using %s for CA file\n' % cafile)
228
228
229 s['cafile'] = cafile
229 s['cafile'] = cafile
230
230
231 # Require certificate validation if CA certs are being loaded and
231 # Require certificate validation if CA certs are being loaded and
232 # verification hasn't been disabled above.
232 # verification hasn't been disabled above.
233 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
233 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
234 s['verifymode'] = ssl.CERT_REQUIRED
234 s['verifymode'] = ssl.CERT_REQUIRED
235 else:
235 else:
236 # At this point we don't have a fingerprint, aren't being
236 # At this point we don't have a fingerprint, aren't being
237 # explicitly insecure, and can't load CA certs. Connecting
237 # explicitly insecure, and can't load CA certs. Connecting
238 # is insecure. We allow the connection and abort during
238 # is insecure. We allow the connection and abort during
239 # validation (once we have the fingerprint to print to the
239 # validation (once we have the fingerprint to print to the
240 # user).
240 # user).
241 s['verifymode'] = ssl.CERT_NONE
241 s['verifymode'] = ssl.CERT_NONE
242
242
243 assert s['protocol'] is not None
243 assert s['protocol'] is not None
244 assert s['ctxoptions'] is not None
244 assert s['ctxoptions'] is not None
245 assert s['verifymode'] is not None
245 assert s['verifymode'] is not None
246
246
247 return s
247 return s
248
248
249 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
249 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
250 """Add SSL/TLS to a socket.
250 """Add SSL/TLS to a socket.
251
251
252 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
252 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
253 choices based on what security options are available.
253 choices based on what security options are available.
254
254
255 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
255 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
256 the following additional arguments:
256 the following additional arguments:
257
257
258 * serverhostname - The expected hostname of the remote server. If the
258 * serverhostname - The expected hostname of the remote server. If the
259 server (and client) support SNI, this tells the server which certificate
259 server (and client) support SNI, this tells the server which certificate
260 to use.
260 to use.
261 """
261 """
262 if not serverhostname:
262 if not serverhostname:
263 raise error.Abort(_('serverhostname argument is required'))
263 raise error.Abort(_('serverhostname argument is required'))
264
264
265 settings = _hostsettings(ui, serverhostname)
265 settings = _hostsettings(ui, serverhostname)
266
266
267 # TODO use ssl.create_default_context() on modernssl.
267 # TODO use ssl.create_default_context() on modernssl.
268 sslcontext = SSLContext(settings['protocol'])
268 sslcontext = SSLContext(settings['protocol'])
269
269
270 # This is a no-op unless using modern ssl.
270 # This is a no-op unless using modern ssl.
271 sslcontext.options |= settings['ctxoptions']
271 sslcontext.options |= settings['ctxoptions']
272
272
273 # This still works on our fake SSLContext.
273 # This still works on our fake SSLContext.
274 sslcontext.verify_mode = settings['verifymode']
274 sslcontext.verify_mode = settings['verifymode']
275
275
276 if certfile is not None:
276 if certfile is not None:
277 def password():
277 def password():
278 f = keyfile or certfile
278 f = keyfile or certfile
279 return ui.getpass(_('passphrase for %s: ') % f, '')
279 return ui.getpass(_('passphrase for %s: ') % f, '')
280 sslcontext.load_cert_chain(certfile, keyfile, password)
280 sslcontext.load_cert_chain(certfile, keyfile, password)
281
281
282 if settings['cafile'] is not None:
282 if settings['cafile'] is not None:
283 try:
283 try:
284 sslcontext.load_verify_locations(cafile=settings['cafile'])
284 sslcontext.load_verify_locations(cafile=settings['cafile'])
285 except ssl.SSLError as e:
285 except ssl.SSLError as e:
286 raise error.Abort(_('error loading CA file %s: %s') % (
286 raise error.Abort(_('error loading CA file %s: %s') % (
287 settings['cafile'], e.args[1]),
287 settings['cafile'], e.args[1]),
288 hint=_('file is empty or malformed?'))
288 hint=_('file is empty or malformed?'))
289 caloaded = True
289 caloaded = True
290 elif settings['allowloaddefaultcerts']:
290 elif settings['allowloaddefaultcerts']:
291 # This is a no-op on old Python.
291 # This is a no-op on old Python.
292 sslcontext.load_default_certs()
292 sslcontext.load_default_certs()
293 caloaded = True
293 caloaded = True
294 else:
294 else:
295 caloaded = False
295 caloaded = False
296
296
297 try:
297 try:
298 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
298 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
299 except ssl.SSLError:
299 except ssl.SSLError:
300 # If we're doing certificate verification and no CA certs are loaded,
300 # If we're doing certificate verification and no CA certs are loaded,
301 # that is almost certainly the reason why verification failed. Provide
301 # that is almost certainly the reason why verification failed. Provide
302 # a hint to the user.
302 # a hint to the user.
303 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
303 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
304 # only show this warning if modern ssl is available.
304 # only show this warning if modern ssl is available.
305 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
305 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
306 modernssl and not sslcontext.get_ca_certs()):
306 modernssl and not sslcontext.get_ca_certs()):
307 ui.warn(_('(an attempt was made to load CA certificates but none '
307 ui.warn(_('(an attempt was made to load CA certificates but none '
308 'were loaded; see '
308 'were loaded; see '
309 'https://mercurial-scm.org/wiki/SecureConnections for '
309 'https://mercurial-scm.org/wiki/SecureConnections for '
310 'how to configure Mercurial to avoid this error)\n'))
310 'how to configure Mercurial to avoid this error)\n'))
311 raise
311 raise
312
312
313 # check if wrap_socket failed silently because socket had been
313 # check if wrap_socket failed silently because socket had been
314 # closed
314 # closed
315 # - see http://bugs.python.org/issue13721
315 # - see http://bugs.python.org/issue13721
316 if not sslsocket.cipher():
316 if not sslsocket.cipher():
317 raise error.Abort(_('ssl connection failed'))
317 raise error.Abort(_('ssl connection failed'))
318
318
319 sslsocket._hgstate = {
319 sslsocket._hgstate = {
320 'caloaded': caloaded,
320 'caloaded': caloaded,
321 'hostname': serverhostname,
321 'hostname': serverhostname,
322 'settings': settings,
322 'settings': settings,
323 'ui': ui,
323 'ui': ui,
324 }
324 }
325
325
326 return sslsocket
326 return sslsocket
327
327
328 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
329 requireclientcert=False):
330 """Wrap a socket for use by servers.
331
332 ``certfile`` and ``keyfile`` specify the files containing the certificate's
333 public and private keys, respectively. Both keys can be defined in the same
334 file via ``certfile`` (the private key must come first in the file).
335
336 ``cafile`` defines the path to certificate authorities.
337
338 ``requireclientcert`` specifies whether to require client certificates.
339
340 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
341 """
342 if modernssl:
343 # We /could/ use create_default_context() here since it doesn't load
344 # CAs when configured for client auth.
345 sslcontext = SSLContext(ssl.PROTOCOL_SSLv23)
346 # SSLv2 and SSLv3 are broken. Ban them outright.
347 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
348 # Prevent CRIME
349 sslcontext.options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
350 # Improve forward secrecy.
351 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
352 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
353
354 # Use the list of more secure ciphers if found in the ssl module.
355 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
356 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
357 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
358 else:
359 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
360
361 if requireclientcert:
362 sslcontext.verify_mode = ssl.CERT_REQUIRED
363 else:
364 sslcontext.verify_mode = ssl.CERT_NONE
365
366 if certfile or keyfile:
367 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
368
369 if cafile:
370 sslcontext.load_verify_locations(cafile=cafile)
371
372 return sslcontext.wrap_socket(sock, server_side=True)
373
328 class wildcarderror(Exception):
374 class wildcarderror(Exception):
329 """Represents an error parsing wildcards in DNS name."""
375 """Represents an error parsing wildcards in DNS name."""
330
376
331 def _dnsnamematch(dn, hostname, maxwildcards=1):
377 def _dnsnamematch(dn, hostname, maxwildcards=1):
332 """Match DNS names according RFC 6125 section 6.4.3.
378 """Match DNS names according RFC 6125 section 6.4.3.
333
379
334 This code is effectively copied from CPython's ssl._dnsname_match.
380 This code is effectively copied from CPython's ssl._dnsname_match.
335
381
336 Returns a bool indicating whether the expected hostname matches
382 Returns a bool indicating whether the expected hostname matches
337 the value in ``dn``.
383 the value in ``dn``.
338 """
384 """
339 pats = []
385 pats = []
340 if not dn:
386 if not dn:
341 return False
387 return False
342
388
343 pieces = dn.split(r'.')
389 pieces = dn.split(r'.')
344 leftmost = pieces[0]
390 leftmost = pieces[0]
345 remainder = pieces[1:]
391 remainder = pieces[1:]
346 wildcards = leftmost.count('*')
392 wildcards = leftmost.count('*')
347 if wildcards > maxwildcards:
393 if wildcards > maxwildcards:
348 raise wildcarderror(
394 raise wildcarderror(
349 _('too many wildcards in certificate DNS name: %s') % dn)
395 _('too many wildcards in certificate DNS name: %s') % dn)
350
396
351 # speed up common case w/o wildcards
397 # speed up common case w/o wildcards
352 if not wildcards:
398 if not wildcards:
353 return dn.lower() == hostname.lower()
399 return dn.lower() == hostname.lower()
354
400
355 # RFC 6125, section 6.4.3, subitem 1.
401 # RFC 6125, section 6.4.3, subitem 1.
356 # The client SHOULD NOT attempt to match a presented identifier in which
402 # The client SHOULD NOT attempt to match a presented identifier in which
357 # the wildcard character comprises a label other than the left-most label.
403 # the wildcard character comprises a label other than the left-most label.
358 if leftmost == '*':
404 if leftmost == '*':
359 # When '*' is a fragment by itself, it matches a non-empty dotless
405 # When '*' is a fragment by itself, it matches a non-empty dotless
360 # fragment.
406 # fragment.
361 pats.append('[^.]+')
407 pats.append('[^.]+')
362 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
408 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
363 # RFC 6125, section 6.4.3, subitem 3.
409 # RFC 6125, section 6.4.3, subitem 3.
364 # The client SHOULD NOT attempt to match a presented identifier
410 # The client SHOULD NOT attempt to match a presented identifier
365 # where the wildcard character is embedded within an A-label or
411 # where the wildcard character is embedded within an A-label or
366 # U-label of an internationalized domain name.
412 # U-label of an internationalized domain name.
367 pats.append(re.escape(leftmost))
413 pats.append(re.escape(leftmost))
368 else:
414 else:
369 # Otherwise, '*' matches any dotless string, e.g. www*
415 # Otherwise, '*' matches any dotless string, e.g. www*
370 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
416 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
371
417
372 # add the remaining fragments, ignore any wildcards
418 # add the remaining fragments, ignore any wildcards
373 for frag in remainder:
419 for frag in remainder:
374 pats.append(re.escape(frag))
420 pats.append(re.escape(frag))
375
421
376 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
422 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
377 return pat.match(hostname) is not None
423 return pat.match(hostname) is not None
378
424
379 def _verifycert(cert, hostname):
425 def _verifycert(cert, hostname):
380 '''Verify that cert (in socket.getpeercert() format) matches hostname.
426 '''Verify that cert (in socket.getpeercert() format) matches hostname.
381 CRLs is not handled.
427 CRLs is not handled.
382
428
383 Returns error message if any problems are found and None on success.
429 Returns error message if any problems are found and None on success.
384 '''
430 '''
385 if not cert:
431 if not cert:
386 return _('no certificate received')
432 return _('no certificate received')
387
433
388 dnsnames = []
434 dnsnames = []
389 san = cert.get('subjectAltName', [])
435 san = cert.get('subjectAltName', [])
390 for key, value in san:
436 for key, value in san:
391 if key == 'DNS':
437 if key == 'DNS':
392 try:
438 try:
393 if _dnsnamematch(value, hostname):
439 if _dnsnamematch(value, hostname):
394 return
440 return
395 except wildcarderror as e:
441 except wildcarderror as e:
396 return e.args[0]
442 return e.args[0]
397
443
398 dnsnames.append(value)
444 dnsnames.append(value)
399
445
400 if not dnsnames:
446 if not dnsnames:
401 # The subject is only checked when there is no DNS in subjectAltName.
447 # The subject is only checked when there is no DNS in subjectAltName.
402 for sub in cert.get('subject', []):
448 for sub in cert.get('subject', []):
403 for key, value in sub:
449 for key, value in sub:
404 # According to RFC 2818 the most specific Common Name must
450 # According to RFC 2818 the most specific Common Name must
405 # be used.
451 # be used.
406 if key == 'commonName':
452 if key == 'commonName':
407 # 'subject' entries are unicide.
453 # 'subject' entries are unicide.
408 try:
454 try:
409 value = value.encode('ascii')
455 value = value.encode('ascii')
410 except UnicodeEncodeError:
456 except UnicodeEncodeError:
411 return _('IDN in certificate not supported')
457 return _('IDN in certificate not supported')
412
458
413 try:
459 try:
414 if _dnsnamematch(value, hostname):
460 if _dnsnamematch(value, hostname):
415 return
461 return
416 except wildcarderror as e:
462 except wildcarderror as e:
417 return e.args[0]
463 return e.args[0]
418
464
419 dnsnames.append(value)
465 dnsnames.append(value)
420
466
421 if len(dnsnames) > 1:
467 if len(dnsnames) > 1:
422 return _('certificate is for %s') % ', '.join(dnsnames)
468 return _('certificate is for %s') % ', '.join(dnsnames)
423 elif len(dnsnames) == 1:
469 elif len(dnsnames) == 1:
424 return _('certificate is for %s') % dnsnames[0]
470 return _('certificate is for %s') % dnsnames[0]
425 else:
471 else:
426 return _('no commonName or subjectAltName found in certificate')
472 return _('no commonName or subjectAltName found in certificate')
427
473
428 def _plainapplepython():
474 def _plainapplepython():
429 """return true if this seems to be a pure Apple Python that
475 """return true if this seems to be a pure Apple Python that
430 * is unfrozen and presumably has the whole mercurial module in the file
476 * is unfrozen and presumably has the whole mercurial module in the file
431 system
477 system
432 * presumably is an Apple Python that uses Apple OpenSSL which has patches
478 * presumably is an Apple Python that uses Apple OpenSSL which has patches
433 for using system certificate store CAs in addition to the provided
479 for using system certificate store CAs in addition to the provided
434 cacerts file
480 cacerts file
435 """
481 """
436 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
482 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
437 return False
483 return False
438 exe = os.path.realpath(sys.executable).lower()
484 exe = os.path.realpath(sys.executable).lower()
439 return (exe.startswith('/usr/bin/python') or
485 return (exe.startswith('/usr/bin/python') or
440 exe.startswith('/system/library/frameworks/python.framework/'))
486 exe.startswith('/system/library/frameworks/python.framework/'))
441
487
442 _systemcacertpaths = [
488 _systemcacertpaths = [
443 # RHEL, CentOS, and Fedora
489 # RHEL, CentOS, and Fedora
444 '/etc/pki/tls/certs/ca-bundle.trust.crt',
490 '/etc/pki/tls/certs/ca-bundle.trust.crt',
445 # Debian, Ubuntu, Gentoo
491 # Debian, Ubuntu, Gentoo
446 '/etc/ssl/certs/ca-certificates.crt',
492 '/etc/ssl/certs/ca-certificates.crt',
447 ]
493 ]
448
494
449 def _defaultcacerts(ui):
495 def _defaultcacerts(ui):
450 """return path to default CA certificates or None.
496 """return path to default CA certificates or None.
451
497
452 It is assumed this function is called when the returned certificates
498 It is assumed this function is called when the returned certificates
453 file will actually be used to validate connections. Therefore this
499 file will actually be used to validate connections. Therefore this
454 function may print warnings or debug messages assuming this usage.
500 function may print warnings or debug messages assuming this usage.
455
501
456 We don't print a message when the Python is able to load default
502 We don't print a message when the Python is able to load default
457 CA certs because this scenario is detected at socket connect time.
503 CA certs because this scenario is detected at socket connect time.
458 """
504 """
459 # The "certifi" Python package provides certificates. If it is installed,
505 # The "certifi" Python package provides certificates. If it is installed,
460 # assume the user intends it to be used and use it.
506 # assume the user intends it to be used and use it.
461 try:
507 try:
462 import certifi
508 import certifi
463 certs = certifi.where()
509 certs = certifi.where()
464 ui.debug('using ca certificates from certifi\n')
510 ui.debug('using ca certificates from certifi\n')
465 return certs
511 return certs
466 except ImportError:
512 except ImportError:
467 pass
513 pass
468
514
469 # On Windows, only the modern ssl module is capable of loading the system
515 # On Windows, only the modern ssl module is capable of loading the system
470 # CA certificates. If we're not capable of doing that, emit a warning
516 # CA certificates. If we're not capable of doing that, emit a warning
471 # because we'll get a certificate verification error later and the lack
517 # because we'll get a certificate verification error later and the lack
472 # of loaded CA certificates will be the reason why.
518 # of loaded CA certificates will be the reason why.
473 # Assertion: this code is only called if certificates are being verified.
519 # Assertion: this code is only called if certificates are being verified.
474 if os.name == 'nt':
520 if os.name == 'nt':
475 if not _canloaddefaultcerts:
521 if not _canloaddefaultcerts:
476 ui.warn(_('(unable to load Windows CA certificates; see '
522 ui.warn(_('(unable to load Windows CA certificates; see '
477 'https://mercurial-scm.org/wiki/SecureConnections for '
523 'https://mercurial-scm.org/wiki/SecureConnections for '
478 'how to configure Mercurial to avoid this message)\n'))
524 'how to configure Mercurial to avoid this message)\n'))
479
525
480 return None
526 return None
481
527
482 # Apple's OpenSSL has patches that allow a specially constructed certificate
528 # Apple's OpenSSL has patches that allow a specially constructed certificate
483 # to load the system CA store. If we're running on Apple Python, use this
529 # to load the system CA store. If we're running on Apple Python, use this
484 # trick.
530 # trick.
485 if _plainapplepython():
531 if _plainapplepython():
486 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
532 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
487 if os.path.exists(dummycert):
533 if os.path.exists(dummycert):
488 return dummycert
534 return dummycert
489
535
490 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
536 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
491 # load system certs, we're out of luck.
537 # load system certs, we're out of luck.
492 if sys.platform == 'darwin':
538 if sys.platform == 'darwin':
493 # FUTURE Consider looking for Homebrew or MacPorts installed certs
539 # FUTURE Consider looking for Homebrew or MacPorts installed certs
494 # files. Also consider exporting the keychain certs to a file during
540 # files. Also consider exporting the keychain certs to a file during
495 # Mercurial install.
541 # Mercurial install.
496 if not _canloaddefaultcerts:
542 if not _canloaddefaultcerts:
497 ui.warn(_('(unable to load CA certificates; see '
543 ui.warn(_('(unable to load CA certificates; see '
498 'https://mercurial-scm.org/wiki/SecureConnections for '
544 'https://mercurial-scm.org/wiki/SecureConnections for '
499 'how to configure Mercurial to avoid this message)\n'))
545 'how to configure Mercurial to avoid this message)\n'))
500 return None
546 return None
501
547
502 # / is writable on Windows. Out of an abundance of caution make sure
548 # / is writable on Windows. Out of an abundance of caution make sure
503 # we're not on Windows because paths from _systemcacerts could be installed
549 # we're not on Windows because paths from _systemcacerts could be installed
504 # by non-admin users.
550 # by non-admin users.
505 assert os.name != 'nt'
551 assert os.name != 'nt'
506
552
507 # Try to find CA certificates in well-known locations. We print a warning
553 # Try to find CA certificates in well-known locations. We print a warning
508 # when using a found file because we don't want too much silent magic
554 # when using a found file because we don't want too much silent magic
509 # for security settings. The expectation is that proper Mercurial
555 # for security settings. The expectation is that proper Mercurial
510 # installs will have the CA certs path defined at install time and the
556 # installs will have the CA certs path defined at install time and the
511 # installer/packager will make an appropriate decision on the user's
557 # installer/packager will make an appropriate decision on the user's
512 # behalf. We only get here and perform this setting as a feature of
558 # behalf. We only get here and perform this setting as a feature of
513 # last resort.
559 # last resort.
514 if not _canloaddefaultcerts:
560 if not _canloaddefaultcerts:
515 for path in _systemcacertpaths:
561 for path in _systemcacertpaths:
516 if os.path.isfile(path):
562 if os.path.isfile(path):
517 ui.warn(_('(using CA certificates from %s; if you see this '
563 ui.warn(_('(using CA certificates from %s; if you see this '
518 'message, your Mercurial install is not properly '
564 'message, your Mercurial install is not properly '
519 'configured; see '
565 'configured; see '
520 'https://mercurial-scm.org/wiki/SecureConnections '
566 'https://mercurial-scm.org/wiki/SecureConnections '
521 'for how to configure Mercurial to avoid this '
567 'for how to configure Mercurial to avoid this '
522 'message)\n') % path)
568 'message)\n') % path)
523 return path
569 return path
524
570
525 ui.warn(_('(unable to load CA certificates; see '
571 ui.warn(_('(unable to load CA certificates; see '
526 'https://mercurial-scm.org/wiki/SecureConnections for '
572 'https://mercurial-scm.org/wiki/SecureConnections for '
527 'how to configure Mercurial to avoid this message)\n'))
573 'how to configure Mercurial to avoid this message)\n'))
528
574
529 return None
575 return None
530
576
531 def validatesocket(sock):
577 def validatesocket(sock):
532 """Validate a socket meets security requiremnets.
578 """Validate a socket meets security requiremnets.
533
579
534 The passed socket must have been created with ``wrapsocket()``.
580 The passed socket must have been created with ``wrapsocket()``.
535 """
581 """
536 host = sock._hgstate['hostname']
582 host = sock._hgstate['hostname']
537 ui = sock._hgstate['ui']
583 ui = sock._hgstate['ui']
538 settings = sock._hgstate['settings']
584 settings = sock._hgstate['settings']
539
585
540 try:
586 try:
541 peercert = sock.getpeercert(True)
587 peercert = sock.getpeercert(True)
542 peercert2 = sock.getpeercert()
588 peercert2 = sock.getpeercert()
543 except AttributeError:
589 except AttributeError:
544 raise error.Abort(_('%s ssl connection error') % host)
590 raise error.Abort(_('%s ssl connection error') % host)
545
591
546 if not peercert:
592 if not peercert:
547 raise error.Abort(_('%s certificate error: '
593 raise error.Abort(_('%s certificate error: '
548 'no certificate received') % host)
594 'no certificate received') % host)
549
595
550 if settings['disablecertverification']:
596 if settings['disablecertverification']:
551 # We don't print the certificate fingerprint because it shouldn't
597 # We don't print the certificate fingerprint because it shouldn't
552 # be necessary: if the user requested certificate verification be
598 # be necessary: if the user requested certificate verification be
553 # disabled, they presumably already saw a message about the inability
599 # disabled, they presumably already saw a message about the inability
554 # to verify the certificate and this message would have printed the
600 # to verify the certificate and this message would have printed the
555 # fingerprint. So printing the fingerprint here adds little to no
601 # fingerprint. So printing the fingerprint here adds little to no
556 # value.
602 # value.
557 ui.warn(_('warning: connection security to %s is disabled per current '
603 ui.warn(_('warning: connection security to %s is disabled per current '
558 'settings; communication is susceptible to eavesdropping '
604 'settings; communication is susceptible to eavesdropping '
559 'and tampering\n') % host)
605 'and tampering\n') % host)
560 return
606 return
561
607
562 # If a certificate fingerprint is pinned, use it and only it to
608 # If a certificate fingerprint is pinned, use it and only it to
563 # validate the remote cert.
609 # validate the remote cert.
564 peerfingerprints = {
610 peerfingerprints = {
565 'sha1': hashlib.sha1(peercert).hexdigest(),
611 'sha1': hashlib.sha1(peercert).hexdigest(),
566 'sha256': hashlib.sha256(peercert).hexdigest(),
612 'sha256': hashlib.sha256(peercert).hexdigest(),
567 'sha512': hashlib.sha512(peercert).hexdigest(),
613 'sha512': hashlib.sha512(peercert).hexdigest(),
568 }
614 }
569
615
570 def fmtfingerprint(s):
616 def fmtfingerprint(s):
571 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
617 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
572
618
573 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
619 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
574
620
575 if settings['certfingerprints']:
621 if settings['certfingerprints']:
576 for hash, fingerprint in settings['certfingerprints']:
622 for hash, fingerprint in settings['certfingerprints']:
577 if peerfingerprints[hash].lower() == fingerprint:
623 if peerfingerprints[hash].lower() == fingerprint:
578 ui.debug('%s certificate matched fingerprint %s:%s\n' %
624 ui.debug('%s certificate matched fingerprint %s:%s\n' %
579 (host, hash, fmtfingerprint(fingerprint)))
625 (host, hash, fmtfingerprint(fingerprint)))
580 return
626 return
581
627
582 # Pinned fingerprint didn't match. This is a fatal error.
628 # Pinned fingerprint didn't match. This is a fatal error.
583 if settings['legacyfingerprint']:
629 if settings['legacyfingerprint']:
584 section = 'hostfingerprint'
630 section = 'hostfingerprint'
585 nice = fmtfingerprint(peerfingerprints['sha1'])
631 nice = fmtfingerprint(peerfingerprints['sha1'])
586 else:
632 else:
587 section = 'hostsecurity'
633 section = 'hostsecurity'
588 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
634 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
589 raise error.Abort(_('certificate for %s has unexpected '
635 raise error.Abort(_('certificate for %s has unexpected '
590 'fingerprint %s') % (host, nice),
636 'fingerprint %s') % (host, nice),
591 hint=_('check %s configuration') % section)
637 hint=_('check %s configuration') % section)
592
638
593 # Security is enabled but no CAs are loaded. We can't establish trust
639 # Security is enabled but no CAs are loaded. We can't establish trust
594 # for the cert so abort.
640 # for the cert so abort.
595 if not sock._hgstate['caloaded']:
641 if not sock._hgstate['caloaded']:
596 raise error.Abort(
642 raise error.Abort(
597 _('unable to verify security of %s (no loaded CA certificates); '
643 _('unable to verify security of %s (no loaded CA certificates); '
598 'refusing to connect') % host,
644 'refusing to connect') % host,
599 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
645 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
600 'how to configure Mercurial to avoid this error or set '
646 'how to configure Mercurial to avoid this error or set '
601 'hostsecurity.%s:fingerprints=%s to trust this server') %
647 'hostsecurity.%s:fingerprints=%s to trust this server') %
602 (host, nicefingerprint))
648 (host, nicefingerprint))
603
649
604 msg = _verifycert(peercert2, host)
650 msg = _verifycert(peercert2, host)
605 if msg:
651 if msg:
606 raise error.Abort(_('%s certificate error: %s') % (host, msg),
652 raise error.Abort(_('%s certificate error: %s') % (host, msg),
607 hint=_('set hostsecurity.%s:certfingerprints=%s '
653 hint=_('set hostsecurity.%s:certfingerprints=%s '
608 'config setting or use --insecure to connect '
654 'config setting or use --insecure to connect '
609 'insecurely') %
655 'insecurely') %
610 (host, nicefingerprint))
656 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now