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