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