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