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