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