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