##// END OF EJS Templates
sslutil: work around SSLContext.get_ca_certs bug on Windows (issue5313)...
Gregory Szorc -
r29631:387bdd53 stable
parent child Browse files
Show More
@@ -1,834 +1,840 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 configprotocols = set([
32 configprotocols = set([
33 'tls1.0',
33 'tls1.0',
34 'tls1.1',
34 'tls1.1',
35 'tls1.2',
35 'tls1.2',
36 ])
36 ])
37
37
38 hassni = getattr(ssl, 'HAS_SNI', False)
38 hassni = getattr(ssl, 'HAS_SNI', False)
39
39
40 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
40 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
41 # against doesn't support them.
41 # against doesn't support them.
42 supportedprotocols = set(['tls1.0'])
42 supportedprotocols = set(['tls1.0'])
43 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
43 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
44 supportedprotocols.add('tls1.1')
44 supportedprotocols.add('tls1.1')
45 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
45 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
46 supportedprotocols.add('tls1.2')
46 supportedprotocols.add('tls1.2')
47
47
48 try:
48 try:
49 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
49 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
50 # SSL/TLS features are available.
50 # SSL/TLS features are available.
51 SSLContext = ssl.SSLContext
51 SSLContext = ssl.SSLContext
52 modernssl = True
52 modernssl = True
53 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
53 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
54 except AttributeError:
54 except AttributeError:
55 modernssl = False
55 modernssl = False
56 _canloaddefaultcerts = False
56 _canloaddefaultcerts = False
57
57
58 # We implement SSLContext using the interface from the standard library.
58 # We implement SSLContext using the interface from the standard library.
59 class SSLContext(object):
59 class SSLContext(object):
60 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
60 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
61 _supportsciphers = sys.version_info >= (2, 7)
61 _supportsciphers = sys.version_info >= (2, 7)
62
62
63 def __init__(self, protocol):
63 def __init__(self, protocol):
64 # From the public interface of SSLContext
64 # From the public interface of SSLContext
65 self.protocol = protocol
65 self.protocol = protocol
66 self.check_hostname = False
66 self.check_hostname = False
67 self.options = 0
67 self.options = 0
68 self.verify_mode = ssl.CERT_NONE
68 self.verify_mode = ssl.CERT_NONE
69
69
70 # Used by our implementation.
70 # Used by our implementation.
71 self._certfile = None
71 self._certfile = None
72 self._keyfile = None
72 self._keyfile = None
73 self._certpassword = None
73 self._certpassword = None
74 self._cacerts = None
74 self._cacerts = None
75 self._ciphers = None
75 self._ciphers = None
76
76
77 def load_cert_chain(self, certfile, keyfile=None, password=None):
77 def load_cert_chain(self, certfile, keyfile=None, password=None):
78 self._certfile = certfile
78 self._certfile = certfile
79 self._keyfile = keyfile
79 self._keyfile = keyfile
80 self._certpassword = password
80 self._certpassword = password
81
81
82 def load_default_certs(self, purpose=None):
82 def load_default_certs(self, purpose=None):
83 pass
83 pass
84
84
85 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
85 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
86 if capath:
86 if capath:
87 raise error.Abort(_('capath not supported'))
87 raise error.Abort(_('capath not supported'))
88 if cadata:
88 if cadata:
89 raise error.Abort(_('cadata not supported'))
89 raise error.Abort(_('cadata not supported'))
90
90
91 self._cacerts = cafile
91 self._cacerts = cafile
92
92
93 def set_ciphers(self, ciphers):
93 def set_ciphers(self, ciphers):
94 if not self._supportsciphers:
94 if not self._supportsciphers:
95 raise error.Abort(_('setting ciphers in [hostsecurity] is not '
95 raise error.Abort(_('setting ciphers in [hostsecurity] is not '
96 'supported by this version of Python'),
96 'supported by this version of Python'),
97 hint=_('remove the config option or run '
97 hint=_('remove the config option or run '
98 'Mercurial with a modern Python '
98 'Mercurial with a modern Python '
99 'version (preferred)'))
99 'version (preferred)'))
100
100
101 self._ciphers = ciphers
101 self._ciphers = ciphers
102
102
103 def wrap_socket(self, socket, server_hostname=None, server_side=False):
103 def wrap_socket(self, socket, server_hostname=None, server_side=False):
104 # server_hostname is unique to SSLContext.wrap_socket and is used
104 # server_hostname is unique to SSLContext.wrap_socket and is used
105 # for SNI in that context. So there's nothing for us to do with it
105 # for SNI in that context. So there's nothing for us to do with it
106 # in this legacy code since we don't support SNI.
106 # in this legacy code since we don't support SNI.
107
107
108 args = {
108 args = {
109 'keyfile': self._keyfile,
109 'keyfile': self._keyfile,
110 'certfile': self._certfile,
110 'certfile': self._certfile,
111 'server_side': server_side,
111 'server_side': server_side,
112 'cert_reqs': self.verify_mode,
112 'cert_reqs': self.verify_mode,
113 'ssl_version': self.protocol,
113 'ssl_version': self.protocol,
114 'ca_certs': self._cacerts,
114 'ca_certs': self._cacerts,
115 }
115 }
116
116
117 if self._supportsciphers:
117 if self._supportsciphers:
118 args['ciphers'] = self._ciphers
118 args['ciphers'] = self._ciphers
119
119
120 return ssl.wrap_socket(socket, **args)
120 return ssl.wrap_socket(socket, **args)
121
121
122 def _hostsettings(ui, hostname):
122 def _hostsettings(ui, hostname):
123 """Obtain security settings for a hostname.
123 """Obtain security settings for a hostname.
124
124
125 Returns a dict of settings relevant to that hostname.
125 Returns a dict of settings relevant to that hostname.
126 """
126 """
127 s = {
127 s = {
128 # Whether we should attempt to load default/available CA certs
128 # Whether we should attempt to load default/available CA certs
129 # if an explicit ``cafile`` is not defined.
129 # if an explicit ``cafile`` is not defined.
130 'allowloaddefaultcerts': True,
130 'allowloaddefaultcerts': True,
131 # List of 2-tuple of (hash algorithm, hash).
131 # List of 2-tuple of (hash algorithm, hash).
132 'certfingerprints': [],
132 'certfingerprints': [],
133 # Path to file containing concatenated CA certs. Used by
133 # Path to file containing concatenated CA certs. Used by
134 # SSLContext.load_verify_locations().
134 # SSLContext.load_verify_locations().
135 'cafile': None,
135 'cafile': None,
136 # Whether certificate verification should be disabled.
136 # Whether certificate verification should be disabled.
137 'disablecertverification': False,
137 'disablecertverification': False,
138 # Whether the legacy [hostfingerprints] section has data for this host.
138 # Whether the legacy [hostfingerprints] section has data for this host.
139 'legacyfingerprint': False,
139 'legacyfingerprint': False,
140 # PROTOCOL_* constant to use for SSLContext.__init__.
140 # PROTOCOL_* constant to use for SSLContext.__init__.
141 'protocol': None,
141 'protocol': None,
142 # String representation of minimum protocol to be used for UI
142 # String representation of minimum protocol to be used for UI
143 # presentation.
143 # presentation.
144 'protocolui': None,
144 'protocolui': None,
145 # ssl.CERT_* constant used by SSLContext.verify_mode.
145 # ssl.CERT_* constant used by SSLContext.verify_mode.
146 'verifymode': None,
146 'verifymode': None,
147 # Defines extra ssl.OP* bitwise options to set.
147 # Defines extra ssl.OP* bitwise options to set.
148 'ctxoptions': None,
148 'ctxoptions': None,
149 # OpenSSL Cipher List to use (instead of default).
149 # OpenSSL Cipher List to use (instead of default).
150 'ciphers': None,
150 'ciphers': None,
151 }
151 }
152
152
153 # Allow minimum TLS protocol to be specified in the config.
153 # Allow minimum TLS protocol to be specified in the config.
154 def validateprotocol(protocol, key):
154 def validateprotocol(protocol, key):
155 if protocol not in configprotocols:
155 if protocol not in configprotocols:
156 raise error.Abort(
156 raise error.Abort(
157 _('unsupported protocol from hostsecurity.%s: %s') %
157 _('unsupported protocol from hostsecurity.%s: %s') %
158 (key, protocol),
158 (key, protocol),
159 hint=_('valid protocols: %s') %
159 hint=_('valid protocols: %s') %
160 ' '.join(sorted(configprotocols)))
160 ' '.join(sorted(configprotocols)))
161
161
162 # We default to TLS 1.1+ where we can because TLS 1.0 has known
162 # We default to TLS 1.1+ where we can because TLS 1.0 has known
163 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
163 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
164 # TLS 1.0+ via config options in case a legacy server is encountered.
164 # TLS 1.0+ via config options in case a legacy server is encountered.
165 if 'tls1.1' in supportedprotocols:
165 if 'tls1.1' in supportedprotocols:
166 defaultprotocol = 'tls1.1'
166 defaultprotocol = 'tls1.1'
167 else:
167 else:
168 # Let people know they are borderline secure.
168 # Let people know they are borderline secure.
169 # We don't document this config option because we want people to see
169 # We don't document this config option because we want people to see
170 # the bold warnings on the web site.
170 # the bold warnings on the web site.
171 # internal config: hostsecurity.disabletls10warning
171 # internal config: hostsecurity.disabletls10warning
172 if not ui.configbool('hostsecurity', 'disabletls10warning'):
172 if not ui.configbool('hostsecurity', 'disabletls10warning'):
173 ui.warn(_('warning: connecting to %s using legacy security '
173 ui.warn(_('warning: connecting to %s using legacy security '
174 'technology (TLS 1.0); see '
174 'technology (TLS 1.0); see '
175 'https://mercurial-scm.org/wiki/SecureConnections for '
175 'https://mercurial-scm.org/wiki/SecureConnections for '
176 'more info\n') % hostname)
176 'more info\n') % hostname)
177 defaultprotocol = 'tls1.0'
177 defaultprotocol = 'tls1.0'
178
178
179 key = 'minimumprotocol'
179 key = 'minimumprotocol'
180 protocol = ui.config('hostsecurity', key, defaultprotocol)
180 protocol = ui.config('hostsecurity', key, defaultprotocol)
181 validateprotocol(protocol, key)
181 validateprotocol(protocol, key)
182
182
183 key = '%s:minimumprotocol' % hostname
183 key = '%s:minimumprotocol' % hostname
184 protocol = ui.config('hostsecurity', key, protocol)
184 protocol = ui.config('hostsecurity', key, protocol)
185 validateprotocol(protocol, key)
185 validateprotocol(protocol, key)
186
186
187 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
187 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
188 # We always print a "connection security to %s is disabled..." message when
188 # We always print a "connection security to %s is disabled..." message when
189 # --insecure is used. So no need to print anything more here.
189 # --insecure is used. So no need to print anything more here.
190 if ui.insecureconnections:
190 if ui.insecureconnections:
191 protocol = 'tls1.0'
191 protocol = 'tls1.0'
192
192
193 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
193 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
194
194
195 ciphers = ui.config('hostsecurity', 'ciphers')
195 ciphers = ui.config('hostsecurity', 'ciphers')
196 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
196 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
197 s['ciphers'] = ciphers
197 s['ciphers'] = ciphers
198
198
199 # Look for fingerprints in [hostsecurity] section. Value is a list
199 # Look for fingerprints in [hostsecurity] section. Value is a list
200 # of <alg>:<fingerprint> strings.
200 # of <alg>:<fingerprint> strings.
201 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
201 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
202 [])
202 [])
203 for fingerprint in fingerprints:
203 for fingerprint in fingerprints:
204 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
204 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
205 raise error.Abort(_('invalid fingerprint for %s: %s') % (
205 raise error.Abort(_('invalid fingerprint for %s: %s') % (
206 hostname, fingerprint),
206 hostname, fingerprint),
207 hint=_('must begin with "sha1:", "sha256:", '
207 hint=_('must begin with "sha1:", "sha256:", '
208 'or "sha512:"'))
208 'or "sha512:"'))
209
209
210 alg, fingerprint = fingerprint.split(':', 1)
210 alg, fingerprint = fingerprint.split(':', 1)
211 fingerprint = fingerprint.replace(':', '').lower()
211 fingerprint = fingerprint.replace(':', '').lower()
212 s['certfingerprints'].append((alg, fingerprint))
212 s['certfingerprints'].append((alg, fingerprint))
213
213
214 # Fingerprints from [hostfingerprints] are always SHA-1.
214 # Fingerprints from [hostfingerprints] are always SHA-1.
215 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
215 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
216 fingerprint = fingerprint.replace(':', '').lower()
216 fingerprint = fingerprint.replace(':', '').lower()
217 s['certfingerprints'].append(('sha1', fingerprint))
217 s['certfingerprints'].append(('sha1', fingerprint))
218 s['legacyfingerprint'] = True
218 s['legacyfingerprint'] = True
219
219
220 # If a host cert fingerprint is defined, it is the only thing that
220 # If a host cert fingerprint is defined, it is the only thing that
221 # matters. No need to validate CA certs.
221 # matters. No need to validate CA certs.
222 if s['certfingerprints']:
222 if s['certfingerprints']:
223 s['verifymode'] = ssl.CERT_NONE
223 s['verifymode'] = ssl.CERT_NONE
224 s['allowloaddefaultcerts'] = False
224 s['allowloaddefaultcerts'] = False
225
225
226 # If --insecure is used, don't take CAs into consideration.
226 # If --insecure is used, don't take CAs into consideration.
227 elif ui.insecureconnections:
227 elif ui.insecureconnections:
228 s['disablecertverification'] = True
228 s['disablecertverification'] = True
229 s['verifymode'] = ssl.CERT_NONE
229 s['verifymode'] = ssl.CERT_NONE
230 s['allowloaddefaultcerts'] = False
230 s['allowloaddefaultcerts'] = False
231
231
232 if ui.configbool('devel', 'disableloaddefaultcerts'):
232 if ui.configbool('devel', 'disableloaddefaultcerts'):
233 s['allowloaddefaultcerts'] = False
233 s['allowloaddefaultcerts'] = False
234
234
235 # If both fingerprints and a per-host ca file are specified, issue a warning
235 # If both fingerprints and a per-host ca file are specified, issue a warning
236 # because users should not be surprised about what security is or isn't
236 # because users should not be surprised about what security is or isn't
237 # being performed.
237 # being performed.
238 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
238 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
239 if s['certfingerprints'] and cafile:
239 if s['certfingerprints'] and cafile:
240 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
240 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
241 'fingerprints defined; using host fingerprints for '
241 'fingerprints defined; using host fingerprints for '
242 'verification)\n') % hostname)
242 'verification)\n') % hostname)
243
243
244 # Try to hook up CA certificate validation unless something above
244 # Try to hook up CA certificate validation unless something above
245 # makes it not necessary.
245 # makes it not necessary.
246 if s['verifymode'] is None:
246 if s['verifymode'] is None:
247 # Look at per-host ca file first.
247 # Look at per-host ca file first.
248 if cafile:
248 if cafile:
249 cafile = util.expandpath(cafile)
249 cafile = util.expandpath(cafile)
250 if not os.path.exists(cafile):
250 if not os.path.exists(cafile):
251 raise error.Abort(_('path specified by %s does not exist: %s') %
251 raise error.Abort(_('path specified by %s does not exist: %s') %
252 ('hostsecurity.%s:verifycertsfile' % hostname,
252 ('hostsecurity.%s:verifycertsfile' % hostname,
253 cafile))
253 cafile))
254 s['cafile'] = cafile
254 s['cafile'] = cafile
255 else:
255 else:
256 # Find global certificates file in config.
256 # Find global certificates file in config.
257 cafile = ui.config('web', 'cacerts')
257 cafile = ui.config('web', 'cacerts')
258
258
259 if cafile:
259 if cafile:
260 cafile = util.expandpath(cafile)
260 cafile = util.expandpath(cafile)
261 if not os.path.exists(cafile):
261 if not os.path.exists(cafile):
262 raise error.Abort(_('could not find web.cacerts: %s') %
262 raise error.Abort(_('could not find web.cacerts: %s') %
263 cafile)
263 cafile)
264 elif s['allowloaddefaultcerts']:
264 elif s['allowloaddefaultcerts']:
265 # CAs not defined in config. Try to find system bundles.
265 # CAs not defined in config. Try to find system bundles.
266 cafile = _defaultcacerts(ui)
266 cafile = _defaultcacerts(ui)
267 if cafile:
267 if cafile:
268 ui.debug('using %s for CA file\n' % cafile)
268 ui.debug('using %s for CA file\n' % cafile)
269
269
270 s['cafile'] = cafile
270 s['cafile'] = cafile
271
271
272 # Require certificate validation if CA certs are being loaded and
272 # Require certificate validation if CA certs are being loaded and
273 # verification hasn't been disabled above.
273 # verification hasn't been disabled above.
274 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
274 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
275 s['verifymode'] = ssl.CERT_REQUIRED
275 s['verifymode'] = ssl.CERT_REQUIRED
276 else:
276 else:
277 # At this point we don't have a fingerprint, aren't being
277 # At this point we don't have a fingerprint, aren't being
278 # explicitly insecure, and can't load CA certs. Connecting
278 # explicitly insecure, and can't load CA certs. Connecting
279 # is insecure. We allow the connection and abort during
279 # is insecure. We allow the connection and abort during
280 # validation (once we have the fingerprint to print to the
280 # validation (once we have the fingerprint to print to the
281 # user).
281 # user).
282 s['verifymode'] = ssl.CERT_NONE
282 s['verifymode'] = ssl.CERT_NONE
283
283
284 assert s['protocol'] is not None
284 assert s['protocol'] is not None
285 assert s['ctxoptions'] is not None
285 assert s['ctxoptions'] is not None
286 assert s['verifymode'] is not None
286 assert s['verifymode'] is not None
287
287
288 return s
288 return s
289
289
290 def protocolsettings(protocol):
290 def protocolsettings(protocol):
291 """Resolve the protocol for a config value.
291 """Resolve the protocol for a config value.
292
292
293 Returns a 3-tuple of (protocol, options, ui value) where the first
293 Returns a 3-tuple of (protocol, options, ui value) where the first
294 2 items are values used by SSLContext and the last is a string value
294 2 items are values used by SSLContext and the last is a string value
295 of the ``minimumprotocol`` config option equivalent.
295 of the ``minimumprotocol`` config option equivalent.
296 """
296 """
297 if protocol not in configprotocols:
297 if protocol not in configprotocols:
298 raise ValueError('protocol value not supported: %s' % protocol)
298 raise ValueError('protocol value not supported: %s' % protocol)
299
299
300 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
300 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
301 # that both ends support, including TLS protocols. On legacy stacks,
301 # that both ends support, including TLS protocols. On legacy stacks,
302 # the highest it likely goes is TLS 1.0. On modern stacks, it can
302 # the highest it likely goes is TLS 1.0. On modern stacks, it can
303 # support TLS 1.2.
303 # support TLS 1.2.
304 #
304 #
305 # The PROTOCOL_TLSv* constants select a specific TLS version
305 # The PROTOCOL_TLSv* constants select a specific TLS version
306 # only (as opposed to multiple versions). So the method for
306 # only (as opposed to multiple versions). So the method for
307 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
307 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
308 # disable protocols via SSLContext.options and OP_NO_* constants.
308 # disable protocols via SSLContext.options and OP_NO_* constants.
309 # However, SSLContext.options doesn't work unless we have the
309 # However, SSLContext.options doesn't work unless we have the
310 # full/real SSLContext available to us.
310 # full/real SSLContext available to us.
311 if supportedprotocols == set(['tls1.0']):
311 if supportedprotocols == set(['tls1.0']):
312 if protocol != 'tls1.0':
312 if protocol != 'tls1.0':
313 raise error.Abort(_('current Python does not support protocol '
313 raise error.Abort(_('current Python does not support protocol '
314 'setting %s') % protocol,
314 'setting %s') % protocol,
315 hint=_('upgrade Python or disable setting since '
315 hint=_('upgrade Python or disable setting since '
316 'only TLS 1.0 is supported'))
316 'only TLS 1.0 is supported'))
317
317
318 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
318 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
319
319
320 # WARNING: returned options don't work unless the modern ssl module
320 # WARNING: returned options don't work unless the modern ssl module
321 # is available. Be careful when adding options here.
321 # is available. Be careful when adding options here.
322
322
323 # SSLv2 and SSLv3 are broken. We ban them outright.
323 # SSLv2 and SSLv3 are broken. We ban them outright.
324 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
324 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
325
325
326 if protocol == 'tls1.0':
326 if protocol == 'tls1.0':
327 # Defaults above are to use TLS 1.0+
327 # Defaults above are to use TLS 1.0+
328 pass
328 pass
329 elif protocol == 'tls1.1':
329 elif protocol == 'tls1.1':
330 options |= ssl.OP_NO_TLSv1
330 options |= ssl.OP_NO_TLSv1
331 elif protocol == 'tls1.2':
331 elif protocol == 'tls1.2':
332 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
332 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
333 else:
333 else:
334 raise error.Abort(_('this should not happen'))
334 raise error.Abort(_('this should not happen'))
335
335
336 # Prevent CRIME.
336 # Prevent CRIME.
337 # There is no guarantee this attribute is defined on the module.
337 # There is no guarantee this attribute is defined on the module.
338 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
338 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
339
339
340 return ssl.PROTOCOL_SSLv23, options, protocol
340 return ssl.PROTOCOL_SSLv23, options, protocol
341
341
342 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
342 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
343 """Add SSL/TLS to a socket.
343 """Add SSL/TLS to a socket.
344
344
345 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
345 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
346 choices based on what security options are available.
346 choices based on what security options are available.
347
347
348 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
348 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
349 the following additional arguments:
349 the following additional arguments:
350
350
351 * serverhostname - The expected hostname of the remote server. If the
351 * serverhostname - The expected hostname of the remote server. If the
352 server (and client) support SNI, this tells the server which certificate
352 server (and client) support SNI, this tells the server which certificate
353 to use.
353 to use.
354 """
354 """
355 if not serverhostname:
355 if not serverhostname:
356 raise error.Abort(_('serverhostname argument is required'))
356 raise error.Abort(_('serverhostname argument is required'))
357
357
358 settings = _hostsettings(ui, serverhostname)
358 settings = _hostsettings(ui, serverhostname)
359
359
360 # We can't use ssl.create_default_context() because it calls
360 # We can't use ssl.create_default_context() because it calls
361 # load_default_certs() unless CA arguments are passed to it. We want to
361 # load_default_certs() unless CA arguments are passed to it. We want to
362 # have explicit control over CA loading because implicitly loading
362 # have explicit control over CA loading because implicitly loading
363 # CAs may undermine the user's intent. For example, a user may define a CA
363 # CAs may undermine the user's intent. For example, a user may define a CA
364 # bundle with a specific CA cert removed. If the system/default CA bundle
364 # bundle with a specific CA cert removed. If the system/default CA bundle
365 # is loaded and contains that removed CA, you've just undone the user's
365 # is loaded and contains that removed CA, you've just undone the user's
366 # choice.
366 # choice.
367 sslcontext = SSLContext(settings['protocol'])
367 sslcontext = SSLContext(settings['protocol'])
368
368
369 # This is a no-op unless using modern ssl.
369 # This is a no-op unless using modern ssl.
370 sslcontext.options |= settings['ctxoptions']
370 sslcontext.options |= settings['ctxoptions']
371
371
372 # This still works on our fake SSLContext.
372 # This still works on our fake SSLContext.
373 sslcontext.verify_mode = settings['verifymode']
373 sslcontext.verify_mode = settings['verifymode']
374
374
375 if settings['ciphers']:
375 if settings['ciphers']:
376 try:
376 try:
377 sslcontext.set_ciphers(settings['ciphers'])
377 sslcontext.set_ciphers(settings['ciphers'])
378 except ssl.SSLError as e:
378 except ssl.SSLError as e:
379 raise error.Abort(_('could not set ciphers: %s') % e.args[0],
379 raise error.Abort(_('could not set ciphers: %s') % e.args[0],
380 hint=_('change cipher string (%s) in config') %
380 hint=_('change cipher string (%s) in config') %
381 settings['ciphers'])
381 settings['ciphers'])
382
382
383 if certfile is not None:
383 if certfile is not None:
384 def password():
384 def password():
385 f = keyfile or certfile
385 f = keyfile or certfile
386 return ui.getpass(_('passphrase for %s: ') % f, '')
386 return ui.getpass(_('passphrase for %s: ') % f, '')
387 sslcontext.load_cert_chain(certfile, keyfile, password)
387 sslcontext.load_cert_chain(certfile, keyfile, password)
388
388
389 if settings['cafile'] is not None:
389 if settings['cafile'] is not None:
390 try:
390 try:
391 sslcontext.load_verify_locations(cafile=settings['cafile'])
391 sslcontext.load_verify_locations(cafile=settings['cafile'])
392 except ssl.SSLError as e:
392 except ssl.SSLError as e:
393 raise error.Abort(_('error loading CA file %s: %s') % (
393 raise error.Abort(_('error loading CA file %s: %s') % (
394 settings['cafile'], e.args[1]),
394 settings['cafile'], e.args[1]),
395 hint=_('file is empty or malformed?'))
395 hint=_('file is empty or malformed?'))
396 caloaded = True
396 caloaded = True
397 elif settings['allowloaddefaultcerts']:
397 elif settings['allowloaddefaultcerts']:
398 # This is a no-op on old Python.
398 # This is a no-op on old Python.
399 sslcontext.load_default_certs()
399 sslcontext.load_default_certs()
400 caloaded = True
400 caloaded = True
401 else:
401 else:
402 caloaded = False
402 caloaded = False
403
403
404 try:
404 try:
405 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
405 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
406 except ssl.SSLError as e:
406 except ssl.SSLError as e:
407 # If we're doing certificate verification and no CA certs are loaded,
407 # If we're doing certificate verification and no CA certs are loaded,
408 # that is almost certainly the reason why verification failed. Provide
408 # that is almost certainly the reason why verification failed. Provide
409 # a hint to the user.
409 # a hint to the user.
410 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
410 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
411 # only show this warning if modern ssl is available.
411 # only show this warning if modern ssl is available.
412 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
412 # The exception handler is here because of
413 modernssl and not sslcontext.get_ca_certs()):
413 # https://bugs.python.org/issue20916.
414 ui.warn(_('(an attempt was made to load CA certificates but none '
414 try:
415 'were loaded; see '
415 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
416 'https://mercurial-scm.org/wiki/SecureConnections for '
416 modernssl and not sslcontext.get_ca_certs()):
417 'how to configure Mercurial to avoid this error)\n'))
417 ui.warn(_('(an attempt was made to load CA certificates but '
418 'none were loaded; see '
419 'https://mercurial-scm.org/wiki/SecureConnections '
420 'for how to configure Mercurial to avoid this '
421 'error)\n'))
422 except ssl.SSLError:
423 pass
418 # Try to print more helpful error messages for known failures.
424 # Try to print more helpful error messages for known failures.
419 if util.safehasattr(e, 'reason'):
425 if util.safehasattr(e, 'reason'):
420 # This error occurs when the client and server don't share a
426 # This error occurs when the client and server don't share a
421 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
427 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
422 # outright. Hopefully the reason for this error is that we require
428 # outright. Hopefully the reason for this error is that we require
423 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
429 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
424 # reason, try to emit an actionable warning.
430 # reason, try to emit an actionable warning.
425 if e.reason == 'UNSUPPORTED_PROTOCOL':
431 if e.reason == 'UNSUPPORTED_PROTOCOL':
426 # We attempted TLS 1.0+.
432 # We attempted TLS 1.0+.
427 if settings['protocolui'] == 'tls1.0':
433 if settings['protocolui'] == 'tls1.0':
428 # We support more than just TLS 1.0+. If this happens,
434 # We support more than just TLS 1.0+. If this happens,
429 # the likely scenario is either the client or the server
435 # the likely scenario is either the client or the server
430 # is really old. (e.g. server doesn't support TLS 1.0+ or
436 # is really old. (e.g. server doesn't support TLS 1.0+ or
431 # client doesn't support modern TLS versions introduced
437 # client doesn't support modern TLS versions introduced
432 # several years from when this comment was written).
438 # several years from when this comment was written).
433 if supportedprotocols != set(['tls1.0']):
439 if supportedprotocols != set(['tls1.0']):
434 ui.warn(_(
440 ui.warn(_(
435 '(could not communicate with %s using security '
441 '(could not communicate with %s using security '
436 'protocols %s; if you are using a modern Mercurial '
442 'protocols %s; if you are using a modern Mercurial '
437 'version, consider contacting the operator of this '
443 'version, consider contacting the operator of this '
438 'server; see '
444 'server; see '
439 'https://mercurial-scm.org/wiki/SecureConnections '
445 'https://mercurial-scm.org/wiki/SecureConnections '
440 'for more info)\n') % (
446 'for more info)\n') % (
441 serverhostname,
447 serverhostname,
442 ', '.join(sorted(supportedprotocols))))
448 ', '.join(sorted(supportedprotocols))))
443 else:
449 else:
444 ui.warn(_(
450 ui.warn(_(
445 '(could not communicate with %s using TLS 1.0; the '
451 '(could not communicate with %s using TLS 1.0; the '
446 'likely cause of this is the server no longer '
452 'likely cause of this is the server no longer '
447 'supports TLS 1.0 because it has known security '
453 'supports TLS 1.0 because it has known security '
448 'vulnerabilities; see '
454 'vulnerabilities; see '
449 'https://mercurial-scm.org/wiki/SecureConnections '
455 'https://mercurial-scm.org/wiki/SecureConnections '
450 'for more info)\n') % serverhostname)
456 'for more info)\n') % serverhostname)
451 else:
457 else:
452 # We attempted TLS 1.1+. We can only get here if the client
458 # We attempted TLS 1.1+. We can only get here if the client
453 # supports the configured protocol. So the likely reason is
459 # supports the configured protocol. So the likely reason is
454 # the client wants better security than the server can
460 # the client wants better security than the server can
455 # offer.
461 # offer.
456 ui.warn(_(
462 ui.warn(_(
457 '(could not negotiate a common security protocol (%s+) '
463 '(could not negotiate a common security protocol (%s+) '
458 'with %s; the likely cause is Mercurial is configured '
464 'with %s; the likely cause is Mercurial is configured '
459 'to be more secure than the server can support)\n') % (
465 'to be more secure than the server can support)\n') % (
460 settings['protocolui'], serverhostname))
466 settings['protocolui'], serverhostname))
461 ui.warn(_('(consider contacting the operator of this '
467 ui.warn(_('(consider contacting the operator of this '
462 'server and ask them to support modern TLS '
468 'server and ask them to support modern TLS '
463 'protocol versions; or, set '
469 'protocol versions; or, set '
464 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
470 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
465 'use of legacy, less secure protocols when '
471 'use of legacy, less secure protocols when '
466 'communicating with this server)\n') %
472 'communicating with this server)\n') %
467 serverhostname)
473 serverhostname)
468 ui.warn(_(
474 ui.warn(_(
469 '(see https://mercurial-scm.org/wiki/SecureConnections '
475 '(see https://mercurial-scm.org/wiki/SecureConnections '
470 'for more info)\n'))
476 'for more info)\n'))
471 raise
477 raise
472
478
473 # check if wrap_socket failed silently because socket had been
479 # check if wrap_socket failed silently because socket had been
474 # closed
480 # closed
475 # - see http://bugs.python.org/issue13721
481 # - see http://bugs.python.org/issue13721
476 if not sslsocket.cipher():
482 if not sslsocket.cipher():
477 raise error.Abort(_('ssl connection failed'))
483 raise error.Abort(_('ssl connection failed'))
478
484
479 sslsocket._hgstate = {
485 sslsocket._hgstate = {
480 'caloaded': caloaded,
486 'caloaded': caloaded,
481 'hostname': serverhostname,
487 'hostname': serverhostname,
482 'settings': settings,
488 'settings': settings,
483 'ui': ui,
489 'ui': ui,
484 }
490 }
485
491
486 return sslsocket
492 return sslsocket
487
493
488 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
494 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
489 requireclientcert=False):
495 requireclientcert=False):
490 """Wrap a socket for use by servers.
496 """Wrap a socket for use by servers.
491
497
492 ``certfile`` and ``keyfile`` specify the files containing the certificate's
498 ``certfile`` and ``keyfile`` specify the files containing the certificate's
493 public and private keys, respectively. Both keys can be defined in the same
499 public and private keys, respectively. Both keys can be defined in the same
494 file via ``certfile`` (the private key must come first in the file).
500 file via ``certfile`` (the private key must come first in the file).
495
501
496 ``cafile`` defines the path to certificate authorities.
502 ``cafile`` defines the path to certificate authorities.
497
503
498 ``requireclientcert`` specifies whether to require client certificates.
504 ``requireclientcert`` specifies whether to require client certificates.
499
505
500 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
506 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
501 """
507 """
502 protocol, options, _protocolui = protocolsettings('tls1.0')
508 protocol, options, _protocolui = protocolsettings('tls1.0')
503
509
504 # This config option is intended for use in tests only. It is a giant
510 # This config option is intended for use in tests only. It is a giant
505 # footgun to kill security. Don't define it.
511 # footgun to kill security. Don't define it.
506 exactprotocol = ui.config('devel', 'serverexactprotocol')
512 exactprotocol = ui.config('devel', 'serverexactprotocol')
507 if exactprotocol == 'tls1.0':
513 if exactprotocol == 'tls1.0':
508 protocol = ssl.PROTOCOL_TLSv1
514 protocol = ssl.PROTOCOL_TLSv1
509 elif exactprotocol == 'tls1.1':
515 elif exactprotocol == 'tls1.1':
510 if 'tls1.1' not in supportedprotocols:
516 if 'tls1.1' not in supportedprotocols:
511 raise error.Abort(_('TLS 1.1 not supported by this Python'))
517 raise error.Abort(_('TLS 1.1 not supported by this Python'))
512 protocol = ssl.PROTOCOL_TLSv1_1
518 protocol = ssl.PROTOCOL_TLSv1_1
513 elif exactprotocol == 'tls1.2':
519 elif exactprotocol == 'tls1.2':
514 if 'tls1.2' not in supportedprotocols:
520 if 'tls1.2' not in supportedprotocols:
515 raise error.Abort(_('TLS 1.2 not supported by this Python'))
521 raise error.Abort(_('TLS 1.2 not supported by this Python'))
516 protocol = ssl.PROTOCOL_TLSv1_2
522 protocol = ssl.PROTOCOL_TLSv1_2
517 elif exactprotocol:
523 elif exactprotocol:
518 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
524 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
519 exactprotocol)
525 exactprotocol)
520
526
521 if modernssl:
527 if modernssl:
522 # We /could/ use create_default_context() here since it doesn't load
528 # We /could/ use create_default_context() here since it doesn't load
523 # CAs when configured for client auth. However, it is hard-coded to
529 # CAs when configured for client auth. However, it is hard-coded to
524 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
530 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
525 sslcontext = SSLContext(protocol)
531 sslcontext = SSLContext(protocol)
526 sslcontext.options |= options
532 sslcontext.options |= options
527
533
528 # Improve forward secrecy.
534 # Improve forward secrecy.
529 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
535 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
530 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
536 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
531
537
532 # Use the list of more secure ciphers if found in the ssl module.
538 # Use the list of more secure ciphers if found in the ssl module.
533 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
539 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
534 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
540 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
535 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
541 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
536 else:
542 else:
537 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
543 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
538
544
539 if requireclientcert:
545 if requireclientcert:
540 sslcontext.verify_mode = ssl.CERT_REQUIRED
546 sslcontext.verify_mode = ssl.CERT_REQUIRED
541 else:
547 else:
542 sslcontext.verify_mode = ssl.CERT_NONE
548 sslcontext.verify_mode = ssl.CERT_NONE
543
549
544 if certfile or keyfile:
550 if certfile or keyfile:
545 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
551 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
546
552
547 if cafile:
553 if cafile:
548 sslcontext.load_verify_locations(cafile=cafile)
554 sslcontext.load_verify_locations(cafile=cafile)
549
555
550 return sslcontext.wrap_socket(sock, server_side=True)
556 return sslcontext.wrap_socket(sock, server_side=True)
551
557
552 class wildcarderror(Exception):
558 class wildcarderror(Exception):
553 """Represents an error parsing wildcards in DNS name."""
559 """Represents an error parsing wildcards in DNS name."""
554
560
555 def _dnsnamematch(dn, hostname, maxwildcards=1):
561 def _dnsnamematch(dn, hostname, maxwildcards=1):
556 """Match DNS names according RFC 6125 section 6.4.3.
562 """Match DNS names according RFC 6125 section 6.4.3.
557
563
558 This code is effectively copied from CPython's ssl._dnsname_match.
564 This code is effectively copied from CPython's ssl._dnsname_match.
559
565
560 Returns a bool indicating whether the expected hostname matches
566 Returns a bool indicating whether the expected hostname matches
561 the value in ``dn``.
567 the value in ``dn``.
562 """
568 """
563 pats = []
569 pats = []
564 if not dn:
570 if not dn:
565 return False
571 return False
566
572
567 pieces = dn.split(r'.')
573 pieces = dn.split(r'.')
568 leftmost = pieces[0]
574 leftmost = pieces[0]
569 remainder = pieces[1:]
575 remainder = pieces[1:]
570 wildcards = leftmost.count('*')
576 wildcards = leftmost.count('*')
571 if wildcards > maxwildcards:
577 if wildcards > maxwildcards:
572 raise wildcarderror(
578 raise wildcarderror(
573 _('too many wildcards in certificate DNS name: %s') % dn)
579 _('too many wildcards in certificate DNS name: %s') % dn)
574
580
575 # speed up common case w/o wildcards
581 # speed up common case w/o wildcards
576 if not wildcards:
582 if not wildcards:
577 return dn.lower() == hostname.lower()
583 return dn.lower() == hostname.lower()
578
584
579 # RFC 6125, section 6.4.3, subitem 1.
585 # RFC 6125, section 6.4.3, subitem 1.
580 # The client SHOULD NOT attempt to match a presented identifier in which
586 # The client SHOULD NOT attempt to match a presented identifier in which
581 # the wildcard character comprises a label other than the left-most label.
587 # the wildcard character comprises a label other than the left-most label.
582 if leftmost == '*':
588 if leftmost == '*':
583 # When '*' is a fragment by itself, it matches a non-empty dotless
589 # When '*' is a fragment by itself, it matches a non-empty dotless
584 # fragment.
590 # fragment.
585 pats.append('[^.]+')
591 pats.append('[^.]+')
586 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
592 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
587 # RFC 6125, section 6.4.3, subitem 3.
593 # RFC 6125, section 6.4.3, subitem 3.
588 # The client SHOULD NOT attempt to match a presented identifier
594 # The client SHOULD NOT attempt to match a presented identifier
589 # where the wildcard character is embedded within an A-label or
595 # where the wildcard character is embedded within an A-label or
590 # U-label of an internationalized domain name.
596 # U-label of an internationalized domain name.
591 pats.append(re.escape(leftmost))
597 pats.append(re.escape(leftmost))
592 else:
598 else:
593 # Otherwise, '*' matches any dotless string, e.g. www*
599 # Otherwise, '*' matches any dotless string, e.g. www*
594 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
600 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
595
601
596 # add the remaining fragments, ignore any wildcards
602 # add the remaining fragments, ignore any wildcards
597 for frag in remainder:
603 for frag in remainder:
598 pats.append(re.escape(frag))
604 pats.append(re.escape(frag))
599
605
600 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
606 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
601 return pat.match(hostname) is not None
607 return pat.match(hostname) is not None
602
608
603 def _verifycert(cert, hostname):
609 def _verifycert(cert, hostname):
604 '''Verify that cert (in socket.getpeercert() format) matches hostname.
610 '''Verify that cert (in socket.getpeercert() format) matches hostname.
605 CRLs is not handled.
611 CRLs is not handled.
606
612
607 Returns error message if any problems are found and None on success.
613 Returns error message if any problems are found and None on success.
608 '''
614 '''
609 if not cert:
615 if not cert:
610 return _('no certificate received')
616 return _('no certificate received')
611
617
612 dnsnames = []
618 dnsnames = []
613 san = cert.get('subjectAltName', [])
619 san = cert.get('subjectAltName', [])
614 for key, value in san:
620 for key, value in san:
615 if key == 'DNS':
621 if key == 'DNS':
616 try:
622 try:
617 if _dnsnamematch(value, hostname):
623 if _dnsnamematch(value, hostname):
618 return
624 return
619 except wildcarderror as e:
625 except wildcarderror as e:
620 return e.args[0]
626 return e.args[0]
621
627
622 dnsnames.append(value)
628 dnsnames.append(value)
623
629
624 if not dnsnames:
630 if not dnsnames:
625 # The subject is only checked when there is no DNS in subjectAltName.
631 # The subject is only checked when there is no DNS in subjectAltName.
626 for sub in cert.get('subject', []):
632 for sub in cert.get('subject', []):
627 for key, value in sub:
633 for key, value in sub:
628 # According to RFC 2818 the most specific Common Name must
634 # According to RFC 2818 the most specific Common Name must
629 # be used.
635 # be used.
630 if key == 'commonName':
636 if key == 'commonName':
631 # 'subject' entries are unicide.
637 # 'subject' entries are unicide.
632 try:
638 try:
633 value = value.encode('ascii')
639 value = value.encode('ascii')
634 except UnicodeEncodeError:
640 except UnicodeEncodeError:
635 return _('IDN in certificate not supported')
641 return _('IDN in certificate not supported')
636
642
637 try:
643 try:
638 if _dnsnamematch(value, hostname):
644 if _dnsnamematch(value, hostname):
639 return
645 return
640 except wildcarderror as e:
646 except wildcarderror as e:
641 return e.args[0]
647 return e.args[0]
642
648
643 dnsnames.append(value)
649 dnsnames.append(value)
644
650
645 if len(dnsnames) > 1:
651 if len(dnsnames) > 1:
646 return _('certificate is for %s') % ', '.join(dnsnames)
652 return _('certificate is for %s') % ', '.join(dnsnames)
647 elif len(dnsnames) == 1:
653 elif len(dnsnames) == 1:
648 return _('certificate is for %s') % dnsnames[0]
654 return _('certificate is for %s') % dnsnames[0]
649 else:
655 else:
650 return _('no commonName or subjectAltName found in certificate')
656 return _('no commonName or subjectAltName found in certificate')
651
657
652 def _plainapplepython():
658 def _plainapplepython():
653 """return true if this seems to be a pure Apple Python that
659 """return true if this seems to be a pure Apple Python that
654 * is unfrozen and presumably has the whole mercurial module in the file
660 * is unfrozen and presumably has the whole mercurial module in the file
655 system
661 system
656 * presumably is an Apple Python that uses Apple OpenSSL which has patches
662 * presumably is an Apple Python that uses Apple OpenSSL which has patches
657 for using system certificate store CAs in addition to the provided
663 for using system certificate store CAs in addition to the provided
658 cacerts file
664 cacerts file
659 """
665 """
660 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
666 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
661 return False
667 return False
662 exe = os.path.realpath(sys.executable).lower()
668 exe = os.path.realpath(sys.executable).lower()
663 return (exe.startswith('/usr/bin/python') or
669 return (exe.startswith('/usr/bin/python') or
664 exe.startswith('/system/library/frameworks/python.framework/'))
670 exe.startswith('/system/library/frameworks/python.framework/'))
665
671
666 _systemcacertpaths = [
672 _systemcacertpaths = [
667 # RHEL, CentOS, and Fedora
673 # RHEL, CentOS, and Fedora
668 '/etc/pki/tls/certs/ca-bundle.trust.crt',
674 '/etc/pki/tls/certs/ca-bundle.trust.crt',
669 # Debian, Ubuntu, Gentoo
675 # Debian, Ubuntu, Gentoo
670 '/etc/ssl/certs/ca-certificates.crt',
676 '/etc/ssl/certs/ca-certificates.crt',
671 ]
677 ]
672
678
673 def _defaultcacerts(ui):
679 def _defaultcacerts(ui):
674 """return path to default CA certificates or None.
680 """return path to default CA certificates or None.
675
681
676 It is assumed this function is called when the returned certificates
682 It is assumed this function is called when the returned certificates
677 file will actually be used to validate connections. Therefore this
683 file will actually be used to validate connections. Therefore this
678 function may print warnings or debug messages assuming this usage.
684 function may print warnings or debug messages assuming this usage.
679
685
680 We don't print a message when the Python is able to load default
686 We don't print a message when the Python is able to load default
681 CA certs because this scenario is detected at socket connect time.
687 CA certs because this scenario is detected at socket connect time.
682 """
688 """
683 # The "certifi" Python package provides certificates. If it is installed,
689 # The "certifi" Python package provides certificates. If it is installed,
684 # assume the user intends it to be used and use it.
690 # assume the user intends it to be used and use it.
685 try:
691 try:
686 import certifi
692 import certifi
687 certs = certifi.where()
693 certs = certifi.where()
688 ui.debug('using ca certificates from certifi\n')
694 ui.debug('using ca certificates from certifi\n')
689 return certs
695 return certs
690 except ImportError:
696 except ImportError:
691 pass
697 pass
692
698
693 # On Windows, only the modern ssl module is capable of loading the system
699 # On Windows, only the modern ssl module is capable of loading the system
694 # CA certificates. If we're not capable of doing that, emit a warning
700 # CA certificates. If we're not capable of doing that, emit a warning
695 # because we'll get a certificate verification error later and the lack
701 # because we'll get a certificate verification error later and the lack
696 # of loaded CA certificates will be the reason why.
702 # of loaded CA certificates will be the reason why.
697 # Assertion: this code is only called if certificates are being verified.
703 # Assertion: this code is only called if certificates are being verified.
698 if os.name == 'nt':
704 if os.name == 'nt':
699 if not _canloaddefaultcerts:
705 if not _canloaddefaultcerts:
700 ui.warn(_('(unable to load Windows CA certificates; see '
706 ui.warn(_('(unable to load Windows CA certificates; see '
701 'https://mercurial-scm.org/wiki/SecureConnections for '
707 'https://mercurial-scm.org/wiki/SecureConnections for '
702 'how to configure Mercurial to avoid this message)\n'))
708 'how to configure Mercurial to avoid this message)\n'))
703
709
704 return None
710 return None
705
711
706 # Apple's OpenSSL has patches that allow a specially constructed certificate
712 # Apple's OpenSSL has patches that allow a specially constructed certificate
707 # to load the system CA store. If we're running on Apple Python, use this
713 # to load the system CA store. If we're running on Apple Python, use this
708 # trick.
714 # trick.
709 if _plainapplepython():
715 if _plainapplepython():
710 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
716 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
711 if os.path.exists(dummycert):
717 if os.path.exists(dummycert):
712 return dummycert
718 return dummycert
713
719
714 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
720 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
715 # load system certs, we're out of luck.
721 # load system certs, we're out of luck.
716 if sys.platform == 'darwin':
722 if sys.platform == 'darwin':
717 # FUTURE Consider looking for Homebrew or MacPorts installed certs
723 # FUTURE Consider looking for Homebrew or MacPorts installed certs
718 # files. Also consider exporting the keychain certs to a file during
724 # files. Also consider exporting the keychain certs to a file during
719 # Mercurial install.
725 # Mercurial install.
720 if not _canloaddefaultcerts:
726 if not _canloaddefaultcerts:
721 ui.warn(_('(unable to load CA certificates; see '
727 ui.warn(_('(unable to load CA certificates; see '
722 'https://mercurial-scm.org/wiki/SecureConnections for '
728 'https://mercurial-scm.org/wiki/SecureConnections for '
723 'how to configure Mercurial to avoid this message)\n'))
729 'how to configure Mercurial to avoid this message)\n'))
724 return None
730 return None
725
731
726 # / is writable on Windows. Out of an abundance of caution make sure
732 # / is writable on Windows. Out of an abundance of caution make sure
727 # we're not on Windows because paths from _systemcacerts could be installed
733 # we're not on Windows because paths from _systemcacerts could be installed
728 # by non-admin users.
734 # by non-admin users.
729 assert os.name != 'nt'
735 assert os.name != 'nt'
730
736
731 # Try to find CA certificates in well-known locations. We print a warning
737 # Try to find CA certificates in well-known locations. We print a warning
732 # when using a found file because we don't want too much silent magic
738 # when using a found file because we don't want too much silent magic
733 # for security settings. The expectation is that proper Mercurial
739 # for security settings. The expectation is that proper Mercurial
734 # installs will have the CA certs path defined at install time and the
740 # installs will have the CA certs path defined at install time and the
735 # installer/packager will make an appropriate decision on the user's
741 # installer/packager will make an appropriate decision on the user's
736 # behalf. We only get here and perform this setting as a feature of
742 # behalf. We only get here and perform this setting as a feature of
737 # last resort.
743 # last resort.
738 if not _canloaddefaultcerts:
744 if not _canloaddefaultcerts:
739 for path in _systemcacertpaths:
745 for path in _systemcacertpaths:
740 if os.path.isfile(path):
746 if os.path.isfile(path):
741 ui.warn(_('(using CA certificates from %s; if you see this '
747 ui.warn(_('(using CA certificates from %s; if you see this '
742 'message, your Mercurial install is not properly '
748 'message, your Mercurial install is not properly '
743 'configured; see '
749 'configured; see '
744 'https://mercurial-scm.org/wiki/SecureConnections '
750 'https://mercurial-scm.org/wiki/SecureConnections '
745 'for how to configure Mercurial to avoid this '
751 'for how to configure Mercurial to avoid this '
746 'message)\n') % path)
752 'message)\n') % path)
747 return path
753 return path
748
754
749 ui.warn(_('(unable to load CA certificates; see '
755 ui.warn(_('(unable to load CA certificates; see '
750 'https://mercurial-scm.org/wiki/SecureConnections for '
756 'https://mercurial-scm.org/wiki/SecureConnections for '
751 'how to configure Mercurial to avoid this message)\n'))
757 'how to configure Mercurial to avoid this message)\n'))
752
758
753 return None
759 return None
754
760
755 def validatesocket(sock):
761 def validatesocket(sock):
756 """Validate a socket meets security requiremnets.
762 """Validate a socket meets security requiremnets.
757
763
758 The passed socket must have been created with ``wrapsocket()``.
764 The passed socket must have been created with ``wrapsocket()``.
759 """
765 """
760 host = sock._hgstate['hostname']
766 host = sock._hgstate['hostname']
761 ui = sock._hgstate['ui']
767 ui = sock._hgstate['ui']
762 settings = sock._hgstate['settings']
768 settings = sock._hgstate['settings']
763
769
764 try:
770 try:
765 peercert = sock.getpeercert(True)
771 peercert = sock.getpeercert(True)
766 peercert2 = sock.getpeercert()
772 peercert2 = sock.getpeercert()
767 except AttributeError:
773 except AttributeError:
768 raise error.Abort(_('%s ssl connection error') % host)
774 raise error.Abort(_('%s ssl connection error') % host)
769
775
770 if not peercert:
776 if not peercert:
771 raise error.Abort(_('%s certificate error: '
777 raise error.Abort(_('%s certificate error: '
772 'no certificate received') % host)
778 'no certificate received') % host)
773
779
774 if settings['disablecertverification']:
780 if settings['disablecertverification']:
775 # We don't print the certificate fingerprint because it shouldn't
781 # We don't print the certificate fingerprint because it shouldn't
776 # be necessary: if the user requested certificate verification be
782 # be necessary: if the user requested certificate verification be
777 # disabled, they presumably already saw a message about the inability
783 # disabled, they presumably already saw a message about the inability
778 # to verify the certificate and this message would have printed the
784 # to verify the certificate and this message would have printed the
779 # fingerprint. So printing the fingerprint here adds little to no
785 # fingerprint. So printing the fingerprint here adds little to no
780 # value.
786 # value.
781 ui.warn(_('warning: connection security to %s is disabled per current '
787 ui.warn(_('warning: connection security to %s is disabled per current '
782 'settings; communication is susceptible to eavesdropping '
788 'settings; communication is susceptible to eavesdropping '
783 'and tampering\n') % host)
789 'and tampering\n') % host)
784 return
790 return
785
791
786 # If a certificate fingerprint is pinned, use it and only it to
792 # If a certificate fingerprint is pinned, use it and only it to
787 # validate the remote cert.
793 # validate the remote cert.
788 peerfingerprints = {
794 peerfingerprints = {
789 'sha1': hashlib.sha1(peercert).hexdigest(),
795 'sha1': hashlib.sha1(peercert).hexdigest(),
790 'sha256': hashlib.sha256(peercert).hexdigest(),
796 'sha256': hashlib.sha256(peercert).hexdigest(),
791 'sha512': hashlib.sha512(peercert).hexdigest(),
797 'sha512': hashlib.sha512(peercert).hexdigest(),
792 }
798 }
793
799
794 def fmtfingerprint(s):
800 def fmtfingerprint(s):
795 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
801 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
796
802
797 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
803 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
798
804
799 if settings['certfingerprints']:
805 if settings['certfingerprints']:
800 for hash, fingerprint in settings['certfingerprints']:
806 for hash, fingerprint in settings['certfingerprints']:
801 if peerfingerprints[hash].lower() == fingerprint:
807 if peerfingerprints[hash].lower() == fingerprint:
802 ui.debug('%s certificate matched fingerprint %s:%s\n' %
808 ui.debug('%s certificate matched fingerprint %s:%s\n' %
803 (host, hash, fmtfingerprint(fingerprint)))
809 (host, hash, fmtfingerprint(fingerprint)))
804 return
810 return
805
811
806 # Pinned fingerprint didn't match. This is a fatal error.
812 # Pinned fingerprint didn't match. This is a fatal error.
807 if settings['legacyfingerprint']:
813 if settings['legacyfingerprint']:
808 section = 'hostfingerprint'
814 section = 'hostfingerprint'
809 nice = fmtfingerprint(peerfingerprints['sha1'])
815 nice = fmtfingerprint(peerfingerprints['sha1'])
810 else:
816 else:
811 section = 'hostsecurity'
817 section = 'hostsecurity'
812 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
818 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
813 raise error.Abort(_('certificate for %s has unexpected '
819 raise error.Abort(_('certificate for %s has unexpected '
814 'fingerprint %s') % (host, nice),
820 'fingerprint %s') % (host, nice),
815 hint=_('check %s configuration') % section)
821 hint=_('check %s configuration') % section)
816
822
817 # Security is enabled but no CAs are loaded. We can't establish trust
823 # Security is enabled but no CAs are loaded. We can't establish trust
818 # for the cert so abort.
824 # for the cert so abort.
819 if not sock._hgstate['caloaded']:
825 if not sock._hgstate['caloaded']:
820 raise error.Abort(
826 raise error.Abort(
821 _('unable to verify security of %s (no loaded CA certificates); '
827 _('unable to verify security of %s (no loaded CA certificates); '
822 'refusing to connect') % host,
828 'refusing to connect') % host,
823 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
829 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
824 'how to configure Mercurial to avoid this error or set '
830 'how to configure Mercurial to avoid this error or set '
825 'hostsecurity.%s:fingerprints=%s to trust this server') %
831 'hostsecurity.%s:fingerprints=%s to trust this server') %
826 (host, nicefingerprint))
832 (host, nicefingerprint))
827
833
828 msg = _verifycert(peercert2, host)
834 msg = _verifycert(peercert2, host)
829 if msg:
835 if msg:
830 raise error.Abort(_('%s certificate error: %s') % (host, msg),
836 raise error.Abort(_('%s certificate error: %s') % (host, msg),
831 hint=_('set hostsecurity.%s:certfingerprints=%s '
837 hint=_('set hostsecurity.%s:certfingerprints=%s '
832 'config setting or use --insecure to connect '
838 'config setting or use --insecure to connect '
833 'insecurely') %
839 'insecurely') %
834 (host, nicefingerprint))
840 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now