##// END OF EJS Templates
sslutil: another use proper attribute to select python 3.7+...
Ondrej Pohorelsky -
r50278:de2e158c default
parent child Browse files
Show More
@@ -1,923 +1,923 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'
125 ciphers = b'DEFAULT'
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, b'reason'):
422 if util.safehasattr(e, b'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, 'PROTOCOL_TLS_SERVER'):
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'serverexactprotocol')
556 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
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 serverexactprotocol: %s') % exactprotocol
586 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
587 )
587 )
588 else:
588 else:
589 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
589 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol that both
590 # ends support, including TLS protocols. commonssloptions() restricts the
590 # ends support, including TLS protocols. commonssloptions() restricts the
591 # set of allowed protocols.
591 # set of allowed protocols.
592 protocol = ssl.PROTOCOL_SSLv23
592 protocol = ssl.PROTOCOL_SSLv23
593 options = commonssloptions(b'tls1.0')
593 options = commonssloptions(b'tls1.0')
594
594
595 # This config option is intended for use in tests only. It is a giant
595 # This config option is intended for use in tests only. It is a giant
596 # footgun to kill security. Don't define it.
596 # footgun to kill security. Don't define it.
597 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
597 exactprotocol = ui.config(b'devel', b'serverexactprotocol')
598 if exactprotocol == b'tls1.0':
598 if exactprotocol == b'tls1.0':
599 if b'tls1.0' not in supportedprotocols:
599 if b'tls1.0' not in supportedprotocols:
600 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
600 raise error.Abort(_(b'TLS 1.0 not supported by this Python'))
601 protocol = ssl.PROTOCOL_TLSv1
601 protocol = ssl.PROTOCOL_TLSv1
602 elif exactprotocol == b'tls1.1':
602 elif exactprotocol == b'tls1.1':
603 if b'tls1.1' not in supportedprotocols:
603 if b'tls1.1' not in supportedprotocols:
604 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
604 raise error.Abort(_(b'TLS 1.1 not supported by this Python'))
605 protocol = ssl.PROTOCOL_TLSv1_1
605 protocol = ssl.PROTOCOL_TLSv1_1
606 elif exactprotocol == b'tls1.2':
606 elif exactprotocol == b'tls1.2':
607 if b'tls1.2' not in supportedprotocols:
607 if b'tls1.2' not in supportedprotocols:
608 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
608 raise error.Abort(_(b'TLS 1.2 not supported by this Python'))
609 protocol = ssl.PROTOCOL_TLSv1_2
609 protocol = ssl.PROTOCOL_TLSv1_2
610 elif exactprotocol:
610 elif exactprotocol:
611 raise error.Abort(
611 raise error.Abort(
612 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
612 _(b'invalid value for serverexactprotocol: %s') % exactprotocol
613 )
613 )
614
614
615 # We /could/ use create_default_context() here since it doesn't load
615 # We /could/ use create_default_context() here since it doesn't load
616 # CAs when configured for client auth. However, it is hard-coded to
616 # CAs when configured for client auth. However, it is hard-coded to
617 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
617 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
618 sslcontext = ssl.SSLContext(protocol)
618 sslcontext = ssl.SSLContext(protocol)
619 sslcontext.options |= options
619 sslcontext.options |= options
620
620
621 # Improve forward secrecy.
621 # Improve forward secrecy.
622 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
622 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
623 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
623 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
624
624
625 # In tests, allow insecure ciphers
625 # In tests, allow insecure ciphers
626 # Otherwise, use the list of more secure ciphers if found in the ssl module.
626 # Otherwise, use the list of more secure ciphers if found in the ssl module.
627 if exactprotocol:
627 if exactprotocol:
628 sslcontext.set_ciphers('DEFAULT')
628 sslcontext.set_ciphers('DEFAULT')
629 elif util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
629 elif util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'):
630 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
630 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
631 # pytype: disable=module-attr
631 # pytype: disable=module-attr
632 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
632 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
633 # pytype: enable=module-attr
633 # pytype: enable=module-attr
634
634
635 if requireclientcert:
635 if requireclientcert:
636 sslcontext.verify_mode = ssl.CERT_REQUIRED
636 sslcontext.verify_mode = ssl.CERT_REQUIRED
637 else:
637 else:
638 sslcontext.verify_mode = ssl.CERT_NONE
638 sslcontext.verify_mode = ssl.CERT_NONE
639
639
640 if certfile or keyfile:
640 if certfile or keyfile:
641 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
641 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
642
642
643 if cafile:
643 if cafile:
644 sslcontext.load_verify_locations(cafile=cafile)
644 sslcontext.load_verify_locations(cafile=cafile)
645
645
646 return sslcontext.wrap_socket(sock, server_side=True)
646 return sslcontext.wrap_socket(sock, server_side=True)
647
647
648
648
649 class wildcarderror(Exception):
649 class wildcarderror(Exception):
650 """Represents an error parsing wildcards in DNS name."""
650 """Represents an error parsing wildcards in DNS name."""
651
651
652
652
653 def _dnsnamematch(dn, hostname, maxwildcards=1):
653 def _dnsnamematch(dn, hostname, maxwildcards=1):
654 """Match DNS names according RFC 6125 section 6.4.3.
654 """Match DNS names according RFC 6125 section 6.4.3.
655
655
656 This code is effectively copied from CPython's ssl._dnsname_match.
656 This code is effectively copied from CPython's ssl._dnsname_match.
657
657
658 Returns a bool indicating whether the expected hostname matches
658 Returns a bool indicating whether the expected hostname matches
659 the value in ``dn``.
659 the value in ``dn``.
660 """
660 """
661 pats = []
661 pats = []
662 if not dn:
662 if not dn:
663 return False
663 return False
664 dn = pycompat.bytesurl(dn)
664 dn = pycompat.bytesurl(dn)
665 hostname = pycompat.bytesurl(hostname)
665 hostname = pycompat.bytesurl(hostname)
666
666
667 pieces = dn.split(b'.')
667 pieces = dn.split(b'.')
668 leftmost = pieces[0]
668 leftmost = pieces[0]
669 remainder = pieces[1:]
669 remainder = pieces[1:]
670 wildcards = leftmost.count(b'*')
670 wildcards = leftmost.count(b'*')
671 if wildcards > maxwildcards:
671 if wildcards > maxwildcards:
672 raise wildcarderror(
672 raise wildcarderror(
673 _(b'too many wildcards in certificate DNS name: %s') % dn
673 _(b'too many wildcards in certificate DNS name: %s') % dn
674 )
674 )
675
675
676 # speed up common case w/o wildcards
676 # speed up common case w/o wildcards
677 if not wildcards:
677 if not wildcards:
678 return dn.lower() == hostname.lower()
678 return dn.lower() == hostname.lower()
679
679
680 # RFC 6125, section 6.4.3, subitem 1.
680 # RFC 6125, section 6.4.3, subitem 1.
681 # The client SHOULD NOT attempt to match a presented identifier in which
681 # The client SHOULD NOT attempt to match a presented identifier in which
682 # the wildcard character comprises a label other than the left-most label.
682 # the wildcard character comprises a label other than the left-most label.
683 if leftmost == b'*':
683 if leftmost == b'*':
684 # When '*' is a fragment by itself, it matches a non-empty dotless
684 # When '*' is a fragment by itself, it matches a non-empty dotless
685 # fragment.
685 # fragment.
686 pats.append(b'[^.]+')
686 pats.append(b'[^.]+')
687 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
687 elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'):
688 # RFC 6125, section 6.4.3, subitem 3.
688 # RFC 6125, section 6.4.3, subitem 3.
689 # The client SHOULD NOT attempt to match a presented identifier
689 # The client SHOULD NOT attempt to match a presented identifier
690 # where the wildcard character is embedded within an A-label or
690 # where the wildcard character is embedded within an A-label or
691 # U-label of an internationalized domain name.
691 # U-label of an internationalized domain name.
692 pats.append(stringutil.reescape(leftmost))
692 pats.append(stringutil.reescape(leftmost))
693 else:
693 else:
694 # Otherwise, '*' matches any dotless string, e.g. www*
694 # Otherwise, '*' matches any dotless string, e.g. www*
695 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
695 pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*'))
696
696
697 # add the remaining fragments, ignore any wildcards
697 # add the remaining fragments, ignore any wildcards
698 for frag in remainder:
698 for frag in remainder:
699 pats.append(stringutil.reescape(frag))
699 pats.append(stringutil.reescape(frag))
700
700
701 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
701 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
702 return pat.match(hostname) is not None
702 return pat.match(hostname) is not None
703
703
704
704
705 def _verifycert(cert, hostname):
705 def _verifycert(cert, hostname):
706 """Verify that cert (in socket.getpeercert() format) matches hostname.
706 """Verify that cert (in socket.getpeercert() format) matches hostname.
707 CRLs is not handled.
707 CRLs is not handled.
708
708
709 Returns error message if any problems are found and None on success.
709 Returns error message if any problems are found and None on success.
710 """
710 """
711 if not cert:
711 if not cert:
712 return _(b'no certificate received')
712 return _(b'no certificate received')
713
713
714 dnsnames = []
714 dnsnames = []
715 san = cert.get('subjectAltName', [])
715 san = cert.get('subjectAltName', [])
716 for key, value in san:
716 for key, value in san:
717 if key == 'DNS':
717 if key == 'DNS':
718 try:
718 try:
719 if _dnsnamematch(value, hostname):
719 if _dnsnamematch(value, hostname):
720 return
720 return
721 except wildcarderror as e:
721 except wildcarderror as e:
722 return stringutil.forcebytestr(e.args[0])
722 return stringutil.forcebytestr(e.args[0])
723
723
724 dnsnames.append(value)
724 dnsnames.append(value)
725
725
726 if not dnsnames:
726 if not dnsnames:
727 # The subject is only checked when there is no DNS in subjectAltName.
727 # The subject is only checked when there is no DNS in subjectAltName.
728 for sub in cert.get('subject', []):
728 for sub in cert.get('subject', []):
729 for key, value in sub:
729 for key, value in sub:
730 # According to RFC 2818 the most specific Common Name must
730 # According to RFC 2818 the most specific Common Name must
731 # be used.
731 # be used.
732 if key == 'commonName':
732 if key == 'commonName':
733 # 'subject' entries are unicode.
733 # 'subject' entries are unicode.
734 try:
734 try:
735 value = value.encode('ascii')
735 value = value.encode('ascii')
736 except UnicodeEncodeError:
736 except UnicodeEncodeError:
737 return _(b'IDN in certificate not supported')
737 return _(b'IDN in certificate not supported')
738
738
739 try:
739 try:
740 if _dnsnamematch(value, hostname):
740 if _dnsnamematch(value, hostname):
741 return
741 return
742 except wildcarderror as e:
742 except wildcarderror as e:
743 return stringutil.forcebytestr(e.args[0])
743 return stringutil.forcebytestr(e.args[0])
744
744
745 dnsnames.append(value)
745 dnsnames.append(value)
746
746
747 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
747 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
748 if len(dnsnames) > 1:
748 if len(dnsnames) > 1:
749 return _(b'certificate is for %s') % b', '.join(dnsnames)
749 return _(b'certificate is for %s') % b', '.join(dnsnames)
750 elif len(dnsnames) == 1:
750 elif len(dnsnames) == 1:
751 return _(b'certificate is for %s') % dnsnames[0]
751 return _(b'certificate is for %s') % dnsnames[0]
752 else:
752 else:
753 return _(b'no commonName or subjectAltName found in certificate')
753 return _(b'no commonName or subjectAltName found in certificate')
754
754
755
755
756 def _plainapplepython():
756 def _plainapplepython():
757 """return true if this seems to be a pure Apple Python that
757 """return true if this seems to be a pure Apple Python that
758 * is unfrozen and presumably has the whole mercurial module in the file
758 * is unfrozen and presumably has the whole mercurial module in the file
759 system
759 system
760 * presumably is an Apple Python that uses Apple OpenSSL which has patches
760 * presumably is an Apple Python that uses Apple OpenSSL which has patches
761 for using system certificate store CAs in addition to the provided
761 for using system certificate store CAs in addition to the provided
762 cacerts file
762 cacerts file
763 """
763 """
764 if (
764 if (
765 not pycompat.isdarwin
765 not pycompat.isdarwin
766 or resourceutil.mainfrozen()
766 or resourceutil.mainfrozen()
767 or not pycompat.sysexecutable
767 or not pycompat.sysexecutable
768 ):
768 ):
769 return False
769 return False
770 exe = os.path.realpath(pycompat.sysexecutable).lower()
770 exe = os.path.realpath(pycompat.sysexecutable).lower()
771 return exe.startswith(b'/usr/bin/python') or exe.startswith(
771 return exe.startswith(b'/usr/bin/python') or exe.startswith(
772 b'/system/library/frameworks/python.framework/'
772 b'/system/library/frameworks/python.framework/'
773 )
773 )
774
774
775
775
776 def _defaultcacerts(ui):
776 def _defaultcacerts(ui):
777 """return path to default CA certificates or None.
777 """return path to default CA certificates or None.
778
778
779 It is assumed this function is called when the returned certificates
779 It is assumed this function is called when the returned certificates
780 file will actually be used to validate connections. Therefore this
780 file will actually be used to validate connections. Therefore this
781 function may print warnings or debug messages assuming this usage.
781 function may print warnings or debug messages assuming this usage.
782
782
783 We don't print a message when the Python is able to load default
783 We don't print a message when the Python is able to load default
784 CA certs because this scenario is detected at socket connect time.
784 CA certs because this scenario is detected at socket connect time.
785 """
785 """
786 # The "certifi" Python package provides certificates. If it is installed
786 # The "certifi" Python package provides certificates. If it is installed
787 # and usable, assume the user intends it to be used and use it.
787 # and usable, assume the user intends it to be used and use it.
788 try:
788 try:
789 import certifi
789 import certifi
790
790
791 certs = certifi.where()
791 certs = certifi.where()
792 if os.path.exists(certs):
792 if os.path.exists(certs):
793 ui.debug(b'using ca certificates from certifi\n')
793 ui.debug(b'using ca certificates from certifi\n')
794 return pycompat.fsencode(certs)
794 return pycompat.fsencode(certs)
795 except (ImportError, AttributeError):
795 except (ImportError, AttributeError):
796 pass
796 pass
797
797
798 # Apple's OpenSSL has patches that allow a specially constructed certificate
798 # Apple's OpenSSL has patches that allow a specially constructed certificate
799 # to load the system CA store. If we're running on Apple Python, use this
799 # to load the system CA store. If we're running on Apple Python, use this
800 # trick.
800 # trick.
801 if _plainapplepython():
801 if _plainapplepython():
802 dummycert = os.path.join(
802 dummycert = os.path.join(
803 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
803 os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem'
804 )
804 )
805 if os.path.exists(dummycert):
805 if os.path.exists(dummycert):
806 return dummycert
806 return dummycert
807
807
808 return None
808 return None
809
809
810
810
811 def validatesocket(sock):
811 def validatesocket(sock):
812 """Validate a socket meets security requirements.
812 """Validate a socket meets security requirements.
813
813
814 The passed socket must have been created with ``wrapsocket()``.
814 The passed socket must have been created with ``wrapsocket()``.
815 """
815 """
816 shost = sock._hgstate[b'hostname']
816 shost = sock._hgstate[b'hostname']
817 host = pycompat.bytesurl(shost)
817 host = pycompat.bytesurl(shost)
818 ui = sock._hgstate[b'ui']
818 ui = sock._hgstate[b'ui']
819 settings = sock._hgstate[b'settings']
819 settings = sock._hgstate[b'settings']
820
820
821 try:
821 try:
822 peercert = sock.getpeercert(True)
822 peercert = sock.getpeercert(True)
823 peercert2 = sock.getpeercert()
823 peercert2 = sock.getpeercert()
824 except AttributeError:
824 except AttributeError:
825 raise error.SecurityError(_(b'%s ssl connection error') % host)
825 raise error.SecurityError(_(b'%s ssl connection error') % host)
826
826
827 if not peercert:
827 if not peercert:
828 raise error.SecurityError(
828 raise error.SecurityError(
829 _(b'%s certificate error: no certificate received') % host
829 _(b'%s certificate error: no certificate received') % host
830 )
830 )
831
831
832 if settings[b'disablecertverification']:
832 if settings[b'disablecertverification']:
833 # We don't print the certificate fingerprint because it shouldn't
833 # We don't print the certificate fingerprint because it shouldn't
834 # be necessary: if the user requested certificate verification be
834 # be necessary: if the user requested certificate verification be
835 # disabled, they presumably already saw a message about the inability
835 # disabled, they presumably already saw a message about the inability
836 # to verify the certificate and this message would have printed the
836 # to verify the certificate and this message would have printed the
837 # fingerprint. So printing the fingerprint here adds little to no
837 # fingerprint. So printing the fingerprint here adds little to no
838 # value.
838 # value.
839 ui.warn(
839 ui.warn(
840 _(
840 _(
841 b'warning: connection security to %s is disabled per current '
841 b'warning: connection security to %s is disabled per current '
842 b'settings; communication is susceptible to eavesdropping '
842 b'settings; communication is susceptible to eavesdropping '
843 b'and tampering\n'
843 b'and tampering\n'
844 )
844 )
845 % host
845 % host
846 )
846 )
847 return
847 return
848
848
849 # If a certificate fingerprint is pinned, use it and only it to
849 # If a certificate fingerprint is pinned, use it and only it to
850 # validate the remote cert.
850 # validate the remote cert.
851 peerfingerprints = {
851 peerfingerprints = {
852 b'sha1': hex(hashutil.sha1(peercert).digest()),
852 b'sha1': hex(hashutil.sha1(peercert).digest()),
853 b'sha256': hex(hashlib.sha256(peercert).digest()),
853 b'sha256': hex(hashlib.sha256(peercert).digest()),
854 b'sha512': hex(hashlib.sha512(peercert).digest()),
854 b'sha512': hex(hashlib.sha512(peercert).digest()),
855 }
855 }
856
856
857 def fmtfingerprint(s):
857 def fmtfingerprint(s):
858 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
858 return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)])
859
859
860 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
860 nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256'])
861
861
862 if settings[b'certfingerprints']:
862 if settings[b'certfingerprints']:
863 for hash, fingerprint in settings[b'certfingerprints']:
863 for hash, fingerprint in settings[b'certfingerprints']:
864 if peerfingerprints[hash].lower() == fingerprint:
864 if peerfingerprints[hash].lower() == fingerprint:
865 ui.debug(
865 ui.debug(
866 b'%s certificate matched fingerprint %s:%s\n'
866 b'%s certificate matched fingerprint %s:%s\n'
867 % (host, hash, fmtfingerprint(fingerprint))
867 % (host, hash, fmtfingerprint(fingerprint))
868 )
868 )
869 if settings[b'legacyfingerprint']:
869 if settings[b'legacyfingerprint']:
870 ui.warn(
870 ui.warn(
871 _(
871 _(
872 b'(SHA-1 fingerprint for %s found in legacy '
872 b'(SHA-1 fingerprint for %s found in legacy '
873 b'[hostfingerprints] section; '
873 b'[hostfingerprints] section; '
874 b'if you trust this fingerprint, remove the old '
874 b'if you trust this fingerprint, remove the old '
875 b'SHA-1 fingerprint from [hostfingerprints] and '
875 b'SHA-1 fingerprint from [hostfingerprints] and '
876 b'add the following entry to the new '
876 b'add the following entry to the new '
877 b'[hostsecurity] section: %s:fingerprints=%s)\n'
877 b'[hostsecurity] section: %s:fingerprints=%s)\n'
878 )
878 )
879 % (host, host, nicefingerprint)
879 % (host, host, nicefingerprint)
880 )
880 )
881 return
881 return
882
882
883 # Pinned fingerprint didn't match. This is a fatal error.
883 # Pinned fingerprint didn't match. This is a fatal error.
884 if settings[b'legacyfingerprint']:
884 if settings[b'legacyfingerprint']:
885 section = b'hostfingerprint'
885 section = b'hostfingerprint'
886 nice = fmtfingerprint(peerfingerprints[b'sha1'])
886 nice = fmtfingerprint(peerfingerprints[b'sha1'])
887 else:
887 else:
888 section = b'hostsecurity'
888 section = b'hostsecurity'
889 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
889 nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
890 raise error.SecurityError(
890 raise error.SecurityError(
891 _(b'certificate for %s has unexpected fingerprint %s')
891 _(b'certificate for %s has unexpected fingerprint %s')
892 % (host, nice),
892 % (host, nice),
893 hint=_(b'check %s configuration') % section,
893 hint=_(b'check %s configuration') % section,
894 )
894 )
895
895
896 # Security is enabled but no CAs are loaded. We can't establish trust
896 # Security is enabled but no CAs are loaded. We can't establish trust
897 # for the cert so abort.
897 # for the cert so abort.
898 if not sock._hgstate[b'caloaded']:
898 if not sock._hgstate[b'caloaded']:
899 raise error.SecurityError(
899 raise error.SecurityError(
900 _(
900 _(
901 b'unable to verify security of %s (no loaded CA certificates); '
901 b'unable to verify security of %s (no loaded CA certificates); '
902 b'refusing to connect'
902 b'refusing to connect'
903 )
903 )
904 % host,
904 % host,
905 hint=_(
905 hint=_(
906 b'see https://mercurial-scm.org/wiki/SecureConnections for '
906 b'see https://mercurial-scm.org/wiki/SecureConnections for '
907 b'how to configure Mercurial to avoid this error or set '
907 b'how to configure Mercurial to avoid this error or set '
908 b'hostsecurity.%s:fingerprints=%s to trust this server'
908 b'hostsecurity.%s:fingerprints=%s to trust this server'
909 )
909 )
910 % (host, nicefingerprint),
910 % (host, nicefingerprint),
911 )
911 )
912
912
913 msg = _verifycert(peercert2, shost)
913 msg = _verifycert(peercert2, shost)
914 if msg:
914 if msg:
915 raise error.SecurityError(
915 raise error.SecurityError(
916 _(b'%s certificate error: %s') % (host, msg),
916 _(b'%s certificate error: %s') % (host, msg),
917 hint=_(
917 hint=_(
918 b'set hostsecurity.%s:certfingerprints=%s '
918 b'set hostsecurity.%s:certfingerprints=%s '
919 b'config setting or use --insecure to connect '
919 b'config setting or use --insecure to connect '
920 b'insecurely'
920 b'insecurely'
921 )
921 )
922 % (host, nicefingerprint),
922 % (host, nicefingerprint),
923 )
923 )
General Comments 0
You need to be logged in to leave comments. Login now