##// END OF EJS Templates
ssl: handle a difference in SSLError with pypy (issue5348)...
Pierre-Yves David -
r29927:799e3674 default
parent child Browse files
Show More
@@ -1,840 +1,844 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 if len(e.args) == 1: # pypy has different SSLError args
394 msg = e.args[0]
395 else:
396 msg = e.args[1]
393 raise error.Abort(_('error loading CA file %s: %s') % (
397 raise error.Abort(_('error loading CA file %s: %s') % (
394 settings['cafile'], e.args[1]),
398 settings['cafile'], msg),
395 hint=_('file is empty or malformed?'))
399 hint=_('file is empty or malformed?'))
396 caloaded = True
400 caloaded = True
397 elif settings['allowloaddefaultcerts']:
401 elif settings['allowloaddefaultcerts']:
398 # This is a no-op on old Python.
402 # This is a no-op on old Python.
399 sslcontext.load_default_certs()
403 sslcontext.load_default_certs()
400 caloaded = True
404 caloaded = True
401 else:
405 else:
402 caloaded = False
406 caloaded = False
403
407
404 try:
408 try:
405 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
409 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
406 except ssl.SSLError as e:
410 except ssl.SSLError as e:
407 # If we're doing certificate verification and no CA certs are loaded,
411 # If we're doing certificate verification and no CA certs are loaded,
408 # that is almost certainly the reason why verification failed. Provide
412 # that is almost certainly the reason why verification failed. Provide
409 # a hint to the user.
413 # a hint to the user.
410 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
414 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
411 # only show this warning if modern ssl is available.
415 # only show this warning if modern ssl is available.
412 # The exception handler is here because of
416 # The exception handler is here because of
413 # https://bugs.python.org/issue20916.
417 # https://bugs.python.org/issue20916.
414 try:
418 try:
415 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
419 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
416 modernssl and not sslcontext.get_ca_certs()):
420 modernssl and not sslcontext.get_ca_certs()):
417 ui.warn(_('(an attempt was made to load CA certificates but '
421 ui.warn(_('(an attempt was made to load CA certificates but '
418 'none were loaded; see '
422 'none were loaded; see '
419 'https://mercurial-scm.org/wiki/SecureConnections '
423 'https://mercurial-scm.org/wiki/SecureConnections '
420 'for how to configure Mercurial to avoid this '
424 'for how to configure Mercurial to avoid this '
421 'error)\n'))
425 'error)\n'))
422 except ssl.SSLError:
426 except ssl.SSLError:
423 pass
427 pass
424 # Try to print more helpful error messages for known failures.
428 # Try to print more helpful error messages for known failures.
425 if util.safehasattr(e, 'reason'):
429 if util.safehasattr(e, 'reason'):
426 # This error occurs when the client and server don't share a
430 # This error occurs when the client and server don't share a
427 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
431 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
428 # outright. Hopefully the reason for this error is that we require
432 # outright. Hopefully the reason for this error is that we require
429 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
433 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
430 # reason, try to emit an actionable warning.
434 # reason, try to emit an actionable warning.
431 if e.reason == 'UNSUPPORTED_PROTOCOL':
435 if e.reason == 'UNSUPPORTED_PROTOCOL':
432 # We attempted TLS 1.0+.
436 # We attempted TLS 1.0+.
433 if settings['protocolui'] == 'tls1.0':
437 if settings['protocolui'] == 'tls1.0':
434 # We support more than just TLS 1.0+. If this happens,
438 # We support more than just TLS 1.0+. If this happens,
435 # the likely scenario is either the client or the server
439 # the likely scenario is either the client or the server
436 # is really old. (e.g. server doesn't support TLS 1.0+ or
440 # is really old. (e.g. server doesn't support TLS 1.0+ or
437 # client doesn't support modern TLS versions introduced
441 # client doesn't support modern TLS versions introduced
438 # several years from when this comment was written).
442 # several years from when this comment was written).
439 if supportedprotocols != set(['tls1.0']):
443 if supportedprotocols != set(['tls1.0']):
440 ui.warn(_(
444 ui.warn(_(
441 '(could not communicate with %s using security '
445 '(could not communicate with %s using security '
442 'protocols %s; if you are using a modern Mercurial '
446 'protocols %s; if you are using a modern Mercurial '
443 'version, consider contacting the operator of this '
447 'version, consider contacting the operator of this '
444 'server; see '
448 'server; see '
445 'https://mercurial-scm.org/wiki/SecureConnections '
449 'https://mercurial-scm.org/wiki/SecureConnections '
446 'for more info)\n') % (
450 'for more info)\n') % (
447 serverhostname,
451 serverhostname,
448 ', '.join(sorted(supportedprotocols))))
452 ', '.join(sorted(supportedprotocols))))
449 else:
453 else:
450 ui.warn(_(
454 ui.warn(_(
451 '(could not communicate with %s using TLS 1.0; the '
455 '(could not communicate with %s using TLS 1.0; the '
452 'likely cause of this is the server no longer '
456 'likely cause of this is the server no longer '
453 'supports TLS 1.0 because it has known security '
457 'supports TLS 1.0 because it has known security '
454 'vulnerabilities; see '
458 'vulnerabilities; see '
455 'https://mercurial-scm.org/wiki/SecureConnections '
459 'https://mercurial-scm.org/wiki/SecureConnections '
456 'for more info)\n') % serverhostname)
460 'for more info)\n') % serverhostname)
457 else:
461 else:
458 # We attempted TLS 1.1+. We can only get here if the client
462 # We attempted TLS 1.1+. We can only get here if the client
459 # supports the configured protocol. So the likely reason is
463 # supports the configured protocol. So the likely reason is
460 # the client wants better security than the server can
464 # the client wants better security than the server can
461 # offer.
465 # offer.
462 ui.warn(_(
466 ui.warn(_(
463 '(could not negotiate a common security protocol (%s+) '
467 '(could not negotiate a common security protocol (%s+) '
464 'with %s; the likely cause is Mercurial is configured '
468 'with %s; the likely cause is Mercurial is configured '
465 'to be more secure than the server can support)\n') % (
469 'to be more secure than the server can support)\n') % (
466 settings['protocolui'], serverhostname))
470 settings['protocolui'], serverhostname))
467 ui.warn(_('(consider contacting the operator of this '
471 ui.warn(_('(consider contacting the operator of this '
468 'server and ask them to support modern TLS '
472 'server and ask them to support modern TLS '
469 'protocol versions; or, set '
473 'protocol versions; or, set '
470 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
474 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
471 'use of legacy, less secure protocols when '
475 'use of legacy, less secure protocols when '
472 'communicating with this server)\n') %
476 'communicating with this server)\n') %
473 serverhostname)
477 serverhostname)
474 ui.warn(_(
478 ui.warn(_(
475 '(see https://mercurial-scm.org/wiki/SecureConnections '
479 '(see https://mercurial-scm.org/wiki/SecureConnections '
476 'for more info)\n'))
480 'for more info)\n'))
477 raise
481 raise
478
482
479 # check if wrap_socket failed silently because socket had been
483 # check if wrap_socket failed silently because socket had been
480 # closed
484 # closed
481 # - see http://bugs.python.org/issue13721
485 # - see http://bugs.python.org/issue13721
482 if not sslsocket.cipher():
486 if not sslsocket.cipher():
483 raise error.Abort(_('ssl connection failed'))
487 raise error.Abort(_('ssl connection failed'))
484
488
485 sslsocket._hgstate = {
489 sslsocket._hgstate = {
486 'caloaded': caloaded,
490 'caloaded': caloaded,
487 'hostname': serverhostname,
491 'hostname': serverhostname,
488 'settings': settings,
492 'settings': settings,
489 'ui': ui,
493 'ui': ui,
490 }
494 }
491
495
492 return sslsocket
496 return sslsocket
493
497
494 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
498 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
495 requireclientcert=False):
499 requireclientcert=False):
496 """Wrap a socket for use by servers.
500 """Wrap a socket for use by servers.
497
501
498 ``certfile`` and ``keyfile`` specify the files containing the certificate's
502 ``certfile`` and ``keyfile`` specify the files containing the certificate's
499 public and private keys, respectively. Both keys can be defined in the same
503 public and private keys, respectively. Both keys can be defined in the same
500 file via ``certfile`` (the private key must come first in the file).
504 file via ``certfile`` (the private key must come first in the file).
501
505
502 ``cafile`` defines the path to certificate authorities.
506 ``cafile`` defines the path to certificate authorities.
503
507
504 ``requireclientcert`` specifies whether to require client certificates.
508 ``requireclientcert`` specifies whether to require client certificates.
505
509
506 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
510 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
507 """
511 """
508 protocol, options, _protocolui = protocolsettings('tls1.0')
512 protocol, options, _protocolui = protocolsettings('tls1.0')
509
513
510 # This config option is intended for use in tests only. It is a giant
514 # This config option is intended for use in tests only. It is a giant
511 # footgun to kill security. Don't define it.
515 # footgun to kill security. Don't define it.
512 exactprotocol = ui.config('devel', 'serverexactprotocol')
516 exactprotocol = ui.config('devel', 'serverexactprotocol')
513 if exactprotocol == 'tls1.0':
517 if exactprotocol == 'tls1.0':
514 protocol = ssl.PROTOCOL_TLSv1
518 protocol = ssl.PROTOCOL_TLSv1
515 elif exactprotocol == 'tls1.1':
519 elif exactprotocol == 'tls1.1':
516 if 'tls1.1' not in supportedprotocols:
520 if 'tls1.1' not in supportedprotocols:
517 raise error.Abort(_('TLS 1.1 not supported by this Python'))
521 raise error.Abort(_('TLS 1.1 not supported by this Python'))
518 protocol = ssl.PROTOCOL_TLSv1_1
522 protocol = ssl.PROTOCOL_TLSv1_1
519 elif exactprotocol == 'tls1.2':
523 elif exactprotocol == 'tls1.2':
520 if 'tls1.2' not in supportedprotocols:
524 if 'tls1.2' not in supportedprotocols:
521 raise error.Abort(_('TLS 1.2 not supported by this Python'))
525 raise error.Abort(_('TLS 1.2 not supported by this Python'))
522 protocol = ssl.PROTOCOL_TLSv1_2
526 protocol = ssl.PROTOCOL_TLSv1_2
523 elif exactprotocol:
527 elif exactprotocol:
524 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
528 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
525 exactprotocol)
529 exactprotocol)
526
530
527 if modernssl:
531 if modernssl:
528 # We /could/ use create_default_context() here since it doesn't load
532 # We /could/ use create_default_context() here since it doesn't load
529 # CAs when configured for client auth. However, it is hard-coded to
533 # CAs when configured for client auth. However, it is hard-coded to
530 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
534 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
531 sslcontext = SSLContext(protocol)
535 sslcontext = SSLContext(protocol)
532 sslcontext.options |= options
536 sslcontext.options |= options
533
537
534 # Improve forward secrecy.
538 # Improve forward secrecy.
535 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
539 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
536 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
540 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
537
541
538 # Use the list of more secure ciphers if found in the ssl module.
542 # Use the list of more secure ciphers if found in the ssl module.
539 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
543 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
540 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
544 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
541 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
545 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
542 else:
546 else:
543 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
547 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
544
548
545 if requireclientcert:
549 if requireclientcert:
546 sslcontext.verify_mode = ssl.CERT_REQUIRED
550 sslcontext.verify_mode = ssl.CERT_REQUIRED
547 else:
551 else:
548 sslcontext.verify_mode = ssl.CERT_NONE
552 sslcontext.verify_mode = ssl.CERT_NONE
549
553
550 if certfile or keyfile:
554 if certfile or keyfile:
551 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
555 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
552
556
553 if cafile:
557 if cafile:
554 sslcontext.load_verify_locations(cafile=cafile)
558 sslcontext.load_verify_locations(cafile=cafile)
555
559
556 return sslcontext.wrap_socket(sock, server_side=True)
560 return sslcontext.wrap_socket(sock, server_side=True)
557
561
558 class wildcarderror(Exception):
562 class wildcarderror(Exception):
559 """Represents an error parsing wildcards in DNS name."""
563 """Represents an error parsing wildcards in DNS name."""
560
564
561 def _dnsnamematch(dn, hostname, maxwildcards=1):
565 def _dnsnamematch(dn, hostname, maxwildcards=1):
562 """Match DNS names according RFC 6125 section 6.4.3.
566 """Match DNS names according RFC 6125 section 6.4.3.
563
567
564 This code is effectively copied from CPython's ssl._dnsname_match.
568 This code is effectively copied from CPython's ssl._dnsname_match.
565
569
566 Returns a bool indicating whether the expected hostname matches
570 Returns a bool indicating whether the expected hostname matches
567 the value in ``dn``.
571 the value in ``dn``.
568 """
572 """
569 pats = []
573 pats = []
570 if not dn:
574 if not dn:
571 return False
575 return False
572
576
573 pieces = dn.split(r'.')
577 pieces = dn.split(r'.')
574 leftmost = pieces[0]
578 leftmost = pieces[0]
575 remainder = pieces[1:]
579 remainder = pieces[1:]
576 wildcards = leftmost.count('*')
580 wildcards = leftmost.count('*')
577 if wildcards > maxwildcards:
581 if wildcards > maxwildcards:
578 raise wildcarderror(
582 raise wildcarderror(
579 _('too many wildcards in certificate DNS name: %s') % dn)
583 _('too many wildcards in certificate DNS name: %s') % dn)
580
584
581 # speed up common case w/o wildcards
585 # speed up common case w/o wildcards
582 if not wildcards:
586 if not wildcards:
583 return dn.lower() == hostname.lower()
587 return dn.lower() == hostname.lower()
584
588
585 # RFC 6125, section 6.4.3, subitem 1.
589 # RFC 6125, section 6.4.3, subitem 1.
586 # The client SHOULD NOT attempt to match a presented identifier in which
590 # The client SHOULD NOT attempt to match a presented identifier in which
587 # the wildcard character comprises a label other than the left-most label.
591 # the wildcard character comprises a label other than the left-most label.
588 if leftmost == '*':
592 if leftmost == '*':
589 # When '*' is a fragment by itself, it matches a non-empty dotless
593 # When '*' is a fragment by itself, it matches a non-empty dotless
590 # fragment.
594 # fragment.
591 pats.append('[^.]+')
595 pats.append('[^.]+')
592 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
596 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
593 # RFC 6125, section 6.4.3, subitem 3.
597 # RFC 6125, section 6.4.3, subitem 3.
594 # The client SHOULD NOT attempt to match a presented identifier
598 # The client SHOULD NOT attempt to match a presented identifier
595 # where the wildcard character is embedded within an A-label or
599 # where the wildcard character is embedded within an A-label or
596 # U-label of an internationalized domain name.
600 # U-label of an internationalized domain name.
597 pats.append(re.escape(leftmost))
601 pats.append(re.escape(leftmost))
598 else:
602 else:
599 # Otherwise, '*' matches any dotless string, e.g. www*
603 # Otherwise, '*' matches any dotless string, e.g. www*
600 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
604 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
601
605
602 # add the remaining fragments, ignore any wildcards
606 # add the remaining fragments, ignore any wildcards
603 for frag in remainder:
607 for frag in remainder:
604 pats.append(re.escape(frag))
608 pats.append(re.escape(frag))
605
609
606 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
610 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
607 return pat.match(hostname) is not None
611 return pat.match(hostname) is not None
608
612
609 def _verifycert(cert, hostname):
613 def _verifycert(cert, hostname):
610 '''Verify that cert (in socket.getpeercert() format) matches hostname.
614 '''Verify that cert (in socket.getpeercert() format) matches hostname.
611 CRLs is not handled.
615 CRLs is not handled.
612
616
613 Returns error message if any problems are found and None on success.
617 Returns error message if any problems are found and None on success.
614 '''
618 '''
615 if not cert:
619 if not cert:
616 return _('no certificate received')
620 return _('no certificate received')
617
621
618 dnsnames = []
622 dnsnames = []
619 san = cert.get('subjectAltName', [])
623 san = cert.get('subjectAltName', [])
620 for key, value in san:
624 for key, value in san:
621 if key == 'DNS':
625 if key == 'DNS':
622 try:
626 try:
623 if _dnsnamematch(value, hostname):
627 if _dnsnamematch(value, hostname):
624 return
628 return
625 except wildcarderror as e:
629 except wildcarderror as e:
626 return e.args[0]
630 return e.args[0]
627
631
628 dnsnames.append(value)
632 dnsnames.append(value)
629
633
630 if not dnsnames:
634 if not dnsnames:
631 # The subject is only checked when there is no DNS in subjectAltName.
635 # The subject is only checked when there is no DNS in subjectAltName.
632 for sub in cert.get('subject', []):
636 for sub in cert.get('subject', []):
633 for key, value in sub:
637 for key, value in sub:
634 # According to RFC 2818 the most specific Common Name must
638 # According to RFC 2818 the most specific Common Name must
635 # be used.
639 # be used.
636 if key == 'commonName':
640 if key == 'commonName':
637 # 'subject' entries are unicide.
641 # 'subject' entries are unicide.
638 try:
642 try:
639 value = value.encode('ascii')
643 value = value.encode('ascii')
640 except UnicodeEncodeError:
644 except UnicodeEncodeError:
641 return _('IDN in certificate not supported')
645 return _('IDN in certificate not supported')
642
646
643 try:
647 try:
644 if _dnsnamematch(value, hostname):
648 if _dnsnamematch(value, hostname):
645 return
649 return
646 except wildcarderror as e:
650 except wildcarderror as e:
647 return e.args[0]
651 return e.args[0]
648
652
649 dnsnames.append(value)
653 dnsnames.append(value)
650
654
651 if len(dnsnames) > 1:
655 if len(dnsnames) > 1:
652 return _('certificate is for %s') % ', '.join(dnsnames)
656 return _('certificate is for %s') % ', '.join(dnsnames)
653 elif len(dnsnames) == 1:
657 elif len(dnsnames) == 1:
654 return _('certificate is for %s') % dnsnames[0]
658 return _('certificate is for %s') % dnsnames[0]
655 else:
659 else:
656 return _('no commonName or subjectAltName found in certificate')
660 return _('no commonName or subjectAltName found in certificate')
657
661
658 def _plainapplepython():
662 def _plainapplepython():
659 """return true if this seems to be a pure Apple Python that
663 """return true if this seems to be a pure Apple Python that
660 * is unfrozen and presumably has the whole mercurial module in the file
664 * is unfrozen and presumably has the whole mercurial module in the file
661 system
665 system
662 * presumably is an Apple Python that uses Apple OpenSSL which has patches
666 * presumably is an Apple Python that uses Apple OpenSSL which has patches
663 for using system certificate store CAs in addition to the provided
667 for using system certificate store CAs in addition to the provided
664 cacerts file
668 cacerts file
665 """
669 """
666 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
670 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
667 return False
671 return False
668 exe = os.path.realpath(sys.executable).lower()
672 exe = os.path.realpath(sys.executable).lower()
669 return (exe.startswith('/usr/bin/python') or
673 return (exe.startswith('/usr/bin/python') or
670 exe.startswith('/system/library/frameworks/python.framework/'))
674 exe.startswith('/system/library/frameworks/python.framework/'))
671
675
672 _systemcacertpaths = [
676 _systemcacertpaths = [
673 # RHEL, CentOS, and Fedora
677 # RHEL, CentOS, and Fedora
674 '/etc/pki/tls/certs/ca-bundle.trust.crt',
678 '/etc/pki/tls/certs/ca-bundle.trust.crt',
675 # Debian, Ubuntu, Gentoo
679 # Debian, Ubuntu, Gentoo
676 '/etc/ssl/certs/ca-certificates.crt',
680 '/etc/ssl/certs/ca-certificates.crt',
677 ]
681 ]
678
682
679 def _defaultcacerts(ui):
683 def _defaultcacerts(ui):
680 """return path to default CA certificates or None.
684 """return path to default CA certificates or None.
681
685
682 It is assumed this function is called when the returned certificates
686 It is assumed this function is called when the returned certificates
683 file will actually be used to validate connections. Therefore this
687 file will actually be used to validate connections. Therefore this
684 function may print warnings or debug messages assuming this usage.
688 function may print warnings or debug messages assuming this usage.
685
689
686 We don't print a message when the Python is able to load default
690 We don't print a message when the Python is able to load default
687 CA certs because this scenario is detected at socket connect time.
691 CA certs because this scenario is detected at socket connect time.
688 """
692 """
689 # The "certifi" Python package provides certificates. If it is installed,
693 # The "certifi" Python package provides certificates. If it is installed,
690 # assume the user intends it to be used and use it.
694 # assume the user intends it to be used and use it.
691 try:
695 try:
692 import certifi
696 import certifi
693 certs = certifi.where()
697 certs = certifi.where()
694 ui.debug('using ca certificates from certifi\n')
698 ui.debug('using ca certificates from certifi\n')
695 return certs
699 return certs
696 except ImportError:
700 except ImportError:
697 pass
701 pass
698
702
699 # On Windows, only the modern ssl module is capable of loading the system
703 # On Windows, only the modern ssl module is capable of loading the system
700 # CA certificates. If we're not capable of doing that, emit a warning
704 # CA certificates. If we're not capable of doing that, emit a warning
701 # because we'll get a certificate verification error later and the lack
705 # because we'll get a certificate verification error later and the lack
702 # of loaded CA certificates will be the reason why.
706 # of loaded CA certificates will be the reason why.
703 # Assertion: this code is only called if certificates are being verified.
707 # Assertion: this code is only called if certificates are being verified.
704 if os.name == 'nt':
708 if os.name == 'nt':
705 if not _canloaddefaultcerts:
709 if not _canloaddefaultcerts:
706 ui.warn(_('(unable to load Windows CA certificates; see '
710 ui.warn(_('(unable to load Windows CA certificates; see '
707 'https://mercurial-scm.org/wiki/SecureConnections for '
711 'https://mercurial-scm.org/wiki/SecureConnections for '
708 'how to configure Mercurial to avoid this message)\n'))
712 'how to configure Mercurial to avoid this message)\n'))
709
713
710 return None
714 return None
711
715
712 # Apple's OpenSSL has patches that allow a specially constructed certificate
716 # Apple's OpenSSL has patches that allow a specially constructed certificate
713 # to load the system CA store. If we're running on Apple Python, use this
717 # to load the system CA store. If we're running on Apple Python, use this
714 # trick.
718 # trick.
715 if _plainapplepython():
719 if _plainapplepython():
716 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
720 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
717 if os.path.exists(dummycert):
721 if os.path.exists(dummycert):
718 return dummycert
722 return dummycert
719
723
720 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
724 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
721 # load system certs, we're out of luck.
725 # load system certs, we're out of luck.
722 if sys.platform == 'darwin':
726 if sys.platform == 'darwin':
723 # FUTURE Consider looking for Homebrew or MacPorts installed certs
727 # FUTURE Consider looking for Homebrew or MacPorts installed certs
724 # files. Also consider exporting the keychain certs to a file during
728 # files. Also consider exporting the keychain certs to a file during
725 # Mercurial install.
729 # Mercurial install.
726 if not _canloaddefaultcerts:
730 if not _canloaddefaultcerts:
727 ui.warn(_('(unable to load CA certificates; see '
731 ui.warn(_('(unable to load CA certificates; see '
728 'https://mercurial-scm.org/wiki/SecureConnections for '
732 'https://mercurial-scm.org/wiki/SecureConnections for '
729 'how to configure Mercurial to avoid this message)\n'))
733 'how to configure Mercurial to avoid this message)\n'))
730 return None
734 return None
731
735
732 # / is writable on Windows. Out of an abundance of caution make sure
736 # / is writable on Windows. Out of an abundance of caution make sure
733 # we're not on Windows because paths from _systemcacerts could be installed
737 # we're not on Windows because paths from _systemcacerts could be installed
734 # by non-admin users.
738 # by non-admin users.
735 assert os.name != 'nt'
739 assert os.name != 'nt'
736
740
737 # Try to find CA certificates in well-known locations. We print a warning
741 # Try to find CA certificates in well-known locations. We print a warning
738 # when using a found file because we don't want too much silent magic
742 # when using a found file because we don't want too much silent magic
739 # for security settings. The expectation is that proper Mercurial
743 # for security settings. The expectation is that proper Mercurial
740 # installs will have the CA certs path defined at install time and the
744 # installs will have the CA certs path defined at install time and the
741 # installer/packager will make an appropriate decision on the user's
745 # installer/packager will make an appropriate decision on the user's
742 # behalf. We only get here and perform this setting as a feature of
746 # behalf. We only get here and perform this setting as a feature of
743 # last resort.
747 # last resort.
744 if not _canloaddefaultcerts:
748 if not _canloaddefaultcerts:
745 for path in _systemcacertpaths:
749 for path in _systemcacertpaths:
746 if os.path.isfile(path):
750 if os.path.isfile(path):
747 ui.warn(_('(using CA certificates from %s; if you see this '
751 ui.warn(_('(using CA certificates from %s; if you see this '
748 'message, your Mercurial install is not properly '
752 'message, your Mercurial install is not properly '
749 'configured; see '
753 'configured; see '
750 'https://mercurial-scm.org/wiki/SecureConnections '
754 'https://mercurial-scm.org/wiki/SecureConnections '
751 'for how to configure Mercurial to avoid this '
755 'for how to configure Mercurial to avoid this '
752 'message)\n') % path)
756 'message)\n') % path)
753 return path
757 return path
754
758
755 ui.warn(_('(unable to load CA certificates; see '
759 ui.warn(_('(unable to load CA certificates; see '
756 'https://mercurial-scm.org/wiki/SecureConnections for '
760 'https://mercurial-scm.org/wiki/SecureConnections for '
757 'how to configure Mercurial to avoid this message)\n'))
761 'how to configure Mercurial to avoid this message)\n'))
758
762
759 return None
763 return None
760
764
761 def validatesocket(sock):
765 def validatesocket(sock):
762 """Validate a socket meets security requiremnets.
766 """Validate a socket meets security requiremnets.
763
767
764 The passed socket must have been created with ``wrapsocket()``.
768 The passed socket must have been created with ``wrapsocket()``.
765 """
769 """
766 host = sock._hgstate['hostname']
770 host = sock._hgstate['hostname']
767 ui = sock._hgstate['ui']
771 ui = sock._hgstate['ui']
768 settings = sock._hgstate['settings']
772 settings = sock._hgstate['settings']
769
773
770 try:
774 try:
771 peercert = sock.getpeercert(True)
775 peercert = sock.getpeercert(True)
772 peercert2 = sock.getpeercert()
776 peercert2 = sock.getpeercert()
773 except AttributeError:
777 except AttributeError:
774 raise error.Abort(_('%s ssl connection error') % host)
778 raise error.Abort(_('%s ssl connection error') % host)
775
779
776 if not peercert:
780 if not peercert:
777 raise error.Abort(_('%s certificate error: '
781 raise error.Abort(_('%s certificate error: '
778 'no certificate received') % host)
782 'no certificate received') % host)
779
783
780 if settings['disablecertverification']:
784 if settings['disablecertverification']:
781 # We don't print the certificate fingerprint because it shouldn't
785 # We don't print the certificate fingerprint because it shouldn't
782 # be necessary: if the user requested certificate verification be
786 # be necessary: if the user requested certificate verification be
783 # disabled, they presumably already saw a message about the inability
787 # disabled, they presumably already saw a message about the inability
784 # to verify the certificate and this message would have printed the
788 # to verify the certificate and this message would have printed the
785 # fingerprint. So printing the fingerprint here adds little to no
789 # fingerprint. So printing the fingerprint here adds little to no
786 # value.
790 # value.
787 ui.warn(_('warning: connection security to %s is disabled per current '
791 ui.warn(_('warning: connection security to %s is disabled per current '
788 'settings; communication is susceptible to eavesdropping '
792 'settings; communication is susceptible to eavesdropping '
789 'and tampering\n') % host)
793 'and tampering\n') % host)
790 return
794 return
791
795
792 # If a certificate fingerprint is pinned, use it and only it to
796 # If a certificate fingerprint is pinned, use it and only it to
793 # validate the remote cert.
797 # validate the remote cert.
794 peerfingerprints = {
798 peerfingerprints = {
795 'sha1': hashlib.sha1(peercert).hexdigest(),
799 'sha1': hashlib.sha1(peercert).hexdigest(),
796 'sha256': hashlib.sha256(peercert).hexdigest(),
800 'sha256': hashlib.sha256(peercert).hexdigest(),
797 'sha512': hashlib.sha512(peercert).hexdigest(),
801 'sha512': hashlib.sha512(peercert).hexdigest(),
798 }
802 }
799
803
800 def fmtfingerprint(s):
804 def fmtfingerprint(s):
801 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
805 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
802
806
803 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
807 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
804
808
805 if settings['certfingerprints']:
809 if settings['certfingerprints']:
806 for hash, fingerprint in settings['certfingerprints']:
810 for hash, fingerprint in settings['certfingerprints']:
807 if peerfingerprints[hash].lower() == fingerprint:
811 if peerfingerprints[hash].lower() == fingerprint:
808 ui.debug('%s certificate matched fingerprint %s:%s\n' %
812 ui.debug('%s certificate matched fingerprint %s:%s\n' %
809 (host, hash, fmtfingerprint(fingerprint)))
813 (host, hash, fmtfingerprint(fingerprint)))
810 return
814 return
811
815
812 # Pinned fingerprint didn't match. This is a fatal error.
816 # Pinned fingerprint didn't match. This is a fatal error.
813 if settings['legacyfingerprint']:
817 if settings['legacyfingerprint']:
814 section = 'hostfingerprint'
818 section = 'hostfingerprint'
815 nice = fmtfingerprint(peerfingerprints['sha1'])
819 nice = fmtfingerprint(peerfingerprints['sha1'])
816 else:
820 else:
817 section = 'hostsecurity'
821 section = 'hostsecurity'
818 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
822 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
819 raise error.Abort(_('certificate for %s has unexpected '
823 raise error.Abort(_('certificate for %s has unexpected '
820 'fingerprint %s') % (host, nice),
824 'fingerprint %s') % (host, nice),
821 hint=_('check %s configuration') % section)
825 hint=_('check %s configuration') % section)
822
826
823 # Security is enabled but no CAs are loaded. We can't establish trust
827 # Security is enabled but no CAs are loaded. We can't establish trust
824 # for the cert so abort.
828 # for the cert so abort.
825 if not sock._hgstate['caloaded']:
829 if not sock._hgstate['caloaded']:
826 raise error.Abort(
830 raise error.Abort(
827 _('unable to verify security of %s (no loaded CA certificates); '
831 _('unable to verify security of %s (no loaded CA certificates); '
828 'refusing to connect') % host,
832 'refusing to connect') % host,
829 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
833 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
830 'how to configure Mercurial to avoid this error or set '
834 'how to configure Mercurial to avoid this error or set '
831 'hostsecurity.%s:fingerprints=%s to trust this server') %
835 'hostsecurity.%s:fingerprints=%s to trust this server') %
832 (host, nicefingerprint))
836 (host, nicefingerprint))
833
837
834 msg = _verifycert(peercert2, host)
838 msg = _verifycert(peercert2, host)
835 if msg:
839 if msg:
836 raise error.Abort(_('%s certificate error: %s') % (host, msg),
840 raise error.Abort(_('%s certificate error: %s') % (host, msg),
837 hint=_('set hostsecurity.%s:certfingerprints=%s '
841 hint=_('set hostsecurity.%s:certfingerprints=%s '
838 'config setting or use --insecure to connect '
842 'config setting or use --insecure to connect '
839 'insecurely') %
843 'insecurely') %
840 (host, nicefingerprint))
844 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now