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