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