##// END OF EJS Templates
sslutil: move context options flags to _hostsettings...
Gregory Szorc -
r29508:d65ec41b default
parent child Browse files
Show More
@@ -1,598 +1,605 b''
1 # sslutil.py - SSL handling for mercurial
1 # sslutil.py - SSL handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import re
14 import re
15 import ssl
15 import ssl
16 import sys
16 import sys
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 error,
20 error,
21 util,
21 util,
22 )
22 )
23
23
24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
26 # all exposed via the "ssl" module.
26 # all exposed via the "ssl" module.
27 #
27 #
28 # Depending on the version of Python being used, SSL/TLS support is either
28 # Depending on the version of Python being used, SSL/TLS support is either
29 # modern/secure or legacy/insecure. Many operations in this module have
29 # modern/secure or legacy/insecure. Many operations in this module have
30 # separate code paths depending on support in Python.
30 # separate code paths depending on support in Python.
31
31
32 hassni = getattr(ssl, 'HAS_SNI', False)
32 hassni = getattr(ssl, 'HAS_SNI', False)
33
33
34 try:
34 try:
35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
37 except AttributeError:
37 except AttributeError:
38 OP_NO_SSLv2 = 0x1000000
38 OP_NO_SSLv2 = 0x1000000
39 OP_NO_SSLv3 = 0x2000000
39 OP_NO_SSLv3 = 0x2000000
40
40
41 try:
41 try:
42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
43 # SSL/TLS features are available.
43 # SSL/TLS features are available.
44 SSLContext = ssl.SSLContext
44 SSLContext = ssl.SSLContext
45 modernssl = True
45 modernssl = True
46 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
46 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
47 except AttributeError:
47 except AttributeError:
48 modernssl = False
48 modernssl = False
49 _canloaddefaultcerts = False
49 _canloaddefaultcerts = False
50
50
51 # We implement SSLContext using the interface from the standard library.
51 # We implement SSLContext using the interface from the standard library.
52 class SSLContext(object):
52 class SSLContext(object):
53 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
53 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
54 _supportsciphers = sys.version_info >= (2, 7)
54 _supportsciphers = sys.version_info >= (2, 7)
55
55
56 def __init__(self, protocol):
56 def __init__(self, protocol):
57 # From the public interface of SSLContext
57 # From the public interface of SSLContext
58 self.protocol = protocol
58 self.protocol = protocol
59 self.check_hostname = False
59 self.check_hostname = False
60 self.options = 0
60 self.options = 0
61 self.verify_mode = ssl.CERT_NONE
61 self.verify_mode = ssl.CERT_NONE
62
62
63 # Used by our implementation.
63 # Used by our implementation.
64 self._certfile = None
64 self._certfile = None
65 self._keyfile = None
65 self._keyfile = None
66 self._certpassword = None
66 self._certpassword = None
67 self._cacerts = None
67 self._cacerts = None
68 self._ciphers = None
68 self._ciphers = None
69
69
70 def load_cert_chain(self, certfile, keyfile=None, password=None):
70 def load_cert_chain(self, certfile, keyfile=None, password=None):
71 self._certfile = certfile
71 self._certfile = certfile
72 self._keyfile = keyfile
72 self._keyfile = keyfile
73 self._certpassword = password
73 self._certpassword = password
74
74
75 def load_default_certs(self, purpose=None):
75 def load_default_certs(self, purpose=None):
76 pass
76 pass
77
77
78 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
79 if capath:
79 if capath:
80 raise error.Abort(_('capath not supported'))
80 raise error.Abort(_('capath not supported'))
81 if cadata:
81 if cadata:
82 raise error.Abort(_('cadata not supported'))
82 raise error.Abort(_('cadata not supported'))
83
83
84 self._cacerts = cafile
84 self._cacerts = cafile
85
85
86 def set_ciphers(self, ciphers):
86 def set_ciphers(self, ciphers):
87 if not self._supportsciphers:
87 if not self._supportsciphers:
88 raise error.Abort(_('setting ciphers not supported'))
88 raise error.Abort(_('setting ciphers not supported'))
89
89
90 self._ciphers = ciphers
90 self._ciphers = ciphers
91
91
92 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 def wrap_socket(self, socket, server_hostname=None, server_side=False):
93 # server_hostname is unique to SSLContext.wrap_socket and is used
93 # server_hostname is unique to SSLContext.wrap_socket and is used
94 # for SNI in that context. So there's nothing for us to do with it
94 # for SNI in that context. So there's nothing for us to do with it
95 # in this legacy code since we don't support SNI.
95 # in this legacy code since we don't support SNI.
96
96
97 args = {
97 args = {
98 'keyfile': self._keyfile,
98 'keyfile': self._keyfile,
99 'certfile': self._certfile,
99 'certfile': self._certfile,
100 'server_side': server_side,
100 'server_side': server_side,
101 'cert_reqs': self.verify_mode,
101 'cert_reqs': self.verify_mode,
102 'ssl_version': self.protocol,
102 'ssl_version': self.protocol,
103 'ca_certs': self._cacerts,
103 'ca_certs': self._cacerts,
104 }
104 }
105
105
106 if self._supportsciphers:
106 if self._supportsciphers:
107 args['ciphers'] = self._ciphers
107 args['ciphers'] = self._ciphers
108
108
109 return ssl.wrap_socket(socket, **args)
109 return ssl.wrap_socket(socket, **args)
110
110
111 def _hostsettings(ui, hostname):
111 def _hostsettings(ui, hostname):
112 """Obtain security settings for a hostname.
112 """Obtain security settings for a hostname.
113
113
114 Returns a dict of settings relevant to that hostname.
114 Returns a dict of settings relevant to that hostname.
115 """
115 """
116 s = {
116 s = {
117 # Whether we should attempt to load default/available CA certs
117 # Whether we should attempt to load default/available CA certs
118 # if an explicit ``cafile`` is not defined.
118 # if an explicit ``cafile`` is not defined.
119 'allowloaddefaultcerts': True,
119 'allowloaddefaultcerts': True,
120 # List of 2-tuple of (hash algorithm, hash).
120 # List of 2-tuple of (hash algorithm, hash).
121 'certfingerprints': [],
121 'certfingerprints': [],
122 # Path to file containing concatenated CA certs. Used by
122 # Path to file containing concatenated CA certs. Used by
123 # SSLContext.load_verify_locations().
123 # SSLContext.load_verify_locations().
124 'cafile': None,
124 'cafile': None,
125 # Whether certificate verification should be disabled.
125 # Whether certificate verification should be disabled.
126 'disablecertverification': False,
126 'disablecertverification': False,
127 # Whether the legacy [hostfingerprints] section has data for this host.
127 # Whether the legacy [hostfingerprints] section has data for this host.
128 'legacyfingerprint': False,
128 'legacyfingerprint': False,
129 # PROTOCOL_* constant to use for SSLContext.__init__.
129 # PROTOCOL_* constant to use for SSLContext.__init__.
130 'protocol': None,
130 'protocol': None,
131 # ssl.CERT_* constant used by SSLContext.verify_mode.
131 # ssl.CERT_* constant used by SSLContext.verify_mode.
132 'verifymode': None,
132 'verifymode': None,
133 # Defines extra ssl.OP* bitwise options to set.
134 'ctxoptions': None,
133 }
135 }
134
136
135 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
137 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
136 # that both ends support, including TLS protocols. On legacy stacks,
138 # that both ends support, including TLS protocols. On legacy stacks,
137 # the highest it likely goes in TLS 1.0. On modern stacks, it can
139 # the highest it likely goes in TLS 1.0. On modern stacks, it can
138 # support TLS 1.2.
140 # support TLS 1.2.
139 #
141 #
140 # The PROTOCOL_TLSv* constants select a specific TLS version
142 # The PROTOCOL_TLSv* constants select a specific TLS version
141 # only (as opposed to multiple versions). So the method for
143 # only (as opposed to multiple versions). So the method for
142 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
144 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
143 # disable protocols via SSLContext.options and OP_NO_* constants.
145 # disable protocols via SSLContext.options and OP_NO_* constants.
144 # However, SSLContext.options doesn't work unless we have the
146 # However, SSLContext.options doesn't work unless we have the
145 # full/real SSLContext available to us.
147 # full/real SSLContext available to us.
146 if modernssl:
148 if modernssl:
147 s['protocol'] = ssl.PROTOCOL_SSLv23
149 s['protocol'] = ssl.PROTOCOL_SSLv23
148 else:
150 else:
149 s['protocol'] = ssl.PROTOCOL_TLSv1
151 s['protocol'] = ssl.PROTOCOL_TLSv1
150
152
153 # SSLv2 and SSLv3 are broken. We ban them outright.
154 # WARNING: ctxoptions doesn't have an effect unless the modern ssl module
155 # is available. Be careful when adding flags!
156 s['ctxoptions'] = OP_NO_SSLv2 | OP_NO_SSLv3
157
151 # Look for fingerprints in [hostsecurity] section. Value is a list
158 # Look for fingerprints in [hostsecurity] section. Value is a list
152 # of <alg>:<fingerprint> strings.
159 # of <alg>:<fingerprint> strings.
153 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
160 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
154 [])
161 [])
155 for fingerprint in fingerprints:
162 for fingerprint in fingerprints:
156 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
163 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
157 raise error.Abort(_('invalid fingerprint for %s: %s') % (
164 raise error.Abort(_('invalid fingerprint for %s: %s') % (
158 hostname, fingerprint),
165 hostname, fingerprint),
159 hint=_('must begin with "sha1:", "sha256:", '
166 hint=_('must begin with "sha1:", "sha256:", '
160 'or "sha512:"'))
167 'or "sha512:"'))
161
168
162 alg, fingerprint = fingerprint.split(':', 1)
169 alg, fingerprint = fingerprint.split(':', 1)
163 fingerprint = fingerprint.replace(':', '').lower()
170 fingerprint = fingerprint.replace(':', '').lower()
164 s['certfingerprints'].append((alg, fingerprint))
171 s['certfingerprints'].append((alg, fingerprint))
165
172
166 # Fingerprints from [hostfingerprints] are always SHA-1.
173 # Fingerprints from [hostfingerprints] are always SHA-1.
167 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
174 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
168 fingerprint = fingerprint.replace(':', '').lower()
175 fingerprint = fingerprint.replace(':', '').lower()
169 s['certfingerprints'].append(('sha1', fingerprint))
176 s['certfingerprints'].append(('sha1', fingerprint))
170 s['legacyfingerprint'] = True
177 s['legacyfingerprint'] = True
171
178
172 # If a host cert fingerprint is defined, it is the only thing that
179 # If a host cert fingerprint is defined, it is the only thing that
173 # matters. No need to validate CA certs.
180 # matters. No need to validate CA certs.
174 if s['certfingerprints']:
181 if s['certfingerprints']:
175 s['verifymode'] = ssl.CERT_NONE
182 s['verifymode'] = ssl.CERT_NONE
176 s['allowloaddefaultcerts'] = False
183 s['allowloaddefaultcerts'] = False
177
184
178 # If --insecure is used, don't take CAs into consideration.
185 # If --insecure is used, don't take CAs into consideration.
179 elif ui.insecureconnections:
186 elif ui.insecureconnections:
180 s['disablecertverification'] = True
187 s['disablecertverification'] = True
181 s['verifymode'] = ssl.CERT_NONE
188 s['verifymode'] = ssl.CERT_NONE
182 s['allowloaddefaultcerts'] = False
189 s['allowloaddefaultcerts'] = False
183
190
184 if ui.configbool('devel', 'disableloaddefaultcerts'):
191 if ui.configbool('devel', 'disableloaddefaultcerts'):
185 s['allowloaddefaultcerts'] = False
192 s['allowloaddefaultcerts'] = False
186
193
187 # If both fingerprints and a per-host ca file are specified, issue a warning
194 # If both fingerprints and a per-host ca file are specified, issue a warning
188 # because users should not be surprised about what security is or isn't
195 # because users should not be surprised about what security is or isn't
189 # being performed.
196 # being performed.
190 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
197 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
191 if s['certfingerprints'] and cafile:
198 if s['certfingerprints'] and cafile:
192 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
199 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
193 'fingerprints defined; using host fingerprints for '
200 'fingerprints defined; using host fingerprints for '
194 'verification)\n') % hostname)
201 'verification)\n') % hostname)
195
202
196 # Try to hook up CA certificate validation unless something above
203 # Try to hook up CA certificate validation unless something above
197 # makes it not necessary.
204 # makes it not necessary.
198 if s['verifymode'] is None:
205 if s['verifymode'] is None:
199 # Look at per-host ca file first.
206 # Look at per-host ca file first.
200 if cafile:
207 if cafile:
201 cafile = util.expandpath(cafile)
208 cafile = util.expandpath(cafile)
202 if not os.path.exists(cafile):
209 if not os.path.exists(cafile):
203 raise error.Abort(_('path specified by %s does not exist: %s') %
210 raise error.Abort(_('path specified by %s does not exist: %s') %
204 ('hostsecurity.%s:verifycertsfile' % hostname,
211 ('hostsecurity.%s:verifycertsfile' % hostname,
205 cafile))
212 cafile))
206 s['cafile'] = cafile
213 s['cafile'] = cafile
207 else:
214 else:
208 # Find global certificates file in config.
215 # Find global certificates file in config.
209 cafile = ui.config('web', 'cacerts')
216 cafile = ui.config('web', 'cacerts')
210
217
211 if cafile:
218 if cafile:
212 cafile = util.expandpath(cafile)
219 cafile = util.expandpath(cafile)
213 if not os.path.exists(cafile):
220 if not os.path.exists(cafile):
214 raise error.Abort(_('could not find web.cacerts: %s') %
221 raise error.Abort(_('could not find web.cacerts: %s') %
215 cafile)
222 cafile)
216 elif s['allowloaddefaultcerts']:
223 elif s['allowloaddefaultcerts']:
217 # CAs not defined in config. Try to find system bundles.
224 # CAs not defined in config. Try to find system bundles.
218 cafile = _defaultcacerts(ui)
225 cafile = _defaultcacerts(ui)
219 if cafile:
226 if cafile:
220 ui.debug('using %s for CA file\n' % cafile)
227 ui.debug('using %s for CA file\n' % cafile)
221
228
222 s['cafile'] = cafile
229 s['cafile'] = cafile
223
230
224 # Require certificate validation if CA certs are being loaded and
231 # Require certificate validation if CA certs are being loaded and
225 # verification hasn't been disabled above.
232 # verification hasn't been disabled above.
226 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
233 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
227 s['verifymode'] = ssl.CERT_REQUIRED
234 s['verifymode'] = ssl.CERT_REQUIRED
228 else:
235 else:
229 # At this point we don't have a fingerprint, aren't being
236 # At this point we don't have a fingerprint, aren't being
230 # explicitly insecure, and can't load CA certs. Connecting
237 # explicitly insecure, and can't load CA certs. Connecting
231 # is insecure. We allow the connection and abort during
238 # is insecure. We allow the connection and abort during
232 # validation (once we have the fingerprint to print to the
239 # validation (once we have the fingerprint to print to the
233 # user).
240 # user).
234 s['verifymode'] = ssl.CERT_NONE
241 s['verifymode'] = ssl.CERT_NONE
235
242
236 assert s['protocol'] is not None
243 assert s['protocol'] is not None
244 assert s['ctxoptions'] is not None
237 assert s['verifymode'] is not None
245 assert s['verifymode'] is not None
238
246
239 return s
247 return s
240
248
241 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
249 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
242 """Add SSL/TLS to a socket.
250 """Add SSL/TLS to a socket.
243
251
244 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
252 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
245 choices based on what security options are available.
253 choices based on what security options are available.
246
254
247 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
255 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
248 the following additional arguments:
256 the following additional arguments:
249
257
250 * serverhostname - The expected hostname of the remote server. If the
258 * serverhostname - The expected hostname of the remote server. If the
251 server (and client) support SNI, this tells the server which certificate
259 server (and client) support SNI, this tells the server which certificate
252 to use.
260 to use.
253 """
261 """
254 if not serverhostname:
262 if not serverhostname:
255 raise error.Abort(_('serverhostname argument is required'))
263 raise error.Abort(_('serverhostname argument is required'))
256
264
257 settings = _hostsettings(ui, serverhostname)
265 settings = _hostsettings(ui, serverhostname)
258
266
259 # TODO use ssl.create_default_context() on modernssl.
267 # TODO use ssl.create_default_context() on modernssl.
260 sslcontext = SSLContext(settings['protocol'])
268 sslcontext = SSLContext(settings['protocol'])
261
269
262 # SSLv2 and SSLv3 are broken. We ban them outright.
270 # This is a no-op unless using modern ssl.
263 # This is a no-op on old Python.
271 sslcontext.options |= settings['ctxoptions']
264 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
265
272
266 # This still works on our fake SSLContext.
273 # This still works on our fake SSLContext.
267 sslcontext.verify_mode = settings['verifymode']
274 sslcontext.verify_mode = settings['verifymode']
268
275
269 if certfile is not None:
276 if certfile is not None:
270 def password():
277 def password():
271 f = keyfile or certfile
278 f = keyfile or certfile
272 return ui.getpass(_('passphrase for %s: ') % f, '')
279 return ui.getpass(_('passphrase for %s: ') % f, '')
273 sslcontext.load_cert_chain(certfile, keyfile, password)
280 sslcontext.load_cert_chain(certfile, keyfile, password)
274
281
275 if settings['cafile'] is not None:
282 if settings['cafile'] is not None:
276 try:
283 try:
277 sslcontext.load_verify_locations(cafile=settings['cafile'])
284 sslcontext.load_verify_locations(cafile=settings['cafile'])
278 except ssl.SSLError as e:
285 except ssl.SSLError as e:
279 raise error.Abort(_('error loading CA file %s: %s') % (
286 raise error.Abort(_('error loading CA file %s: %s') % (
280 settings['cafile'], e.args[1]),
287 settings['cafile'], e.args[1]),
281 hint=_('file is empty or malformed?'))
288 hint=_('file is empty or malformed?'))
282 caloaded = True
289 caloaded = True
283 elif settings['allowloaddefaultcerts']:
290 elif settings['allowloaddefaultcerts']:
284 # This is a no-op on old Python.
291 # This is a no-op on old Python.
285 sslcontext.load_default_certs()
292 sslcontext.load_default_certs()
286 caloaded = True
293 caloaded = True
287 else:
294 else:
288 caloaded = False
295 caloaded = False
289
296
290 try:
297 try:
291 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
298 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
292 except ssl.SSLError:
299 except ssl.SSLError:
293 # If we're doing certificate verification and no CA certs are loaded,
300 # If we're doing certificate verification and no CA certs are loaded,
294 # that is almost certainly the reason why verification failed. Provide
301 # that is almost certainly the reason why verification failed. Provide
295 # a hint to the user.
302 # a hint to the user.
296 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
303 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
297 # only show this warning if modern ssl is available.
304 # only show this warning if modern ssl is available.
298 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
305 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
299 modernssl and not sslcontext.get_ca_certs()):
306 modernssl and not sslcontext.get_ca_certs()):
300 ui.warn(_('(an attempt was made to load CA certificates but none '
307 ui.warn(_('(an attempt was made to load CA certificates but none '
301 'were loaded; see '
308 'were loaded; see '
302 'https://mercurial-scm.org/wiki/SecureConnections for '
309 'https://mercurial-scm.org/wiki/SecureConnections for '
303 'how to configure Mercurial to avoid this error)\n'))
310 'how to configure Mercurial to avoid this error)\n'))
304 raise
311 raise
305
312
306 # check if wrap_socket failed silently because socket had been
313 # check if wrap_socket failed silently because socket had been
307 # closed
314 # closed
308 # - see http://bugs.python.org/issue13721
315 # - see http://bugs.python.org/issue13721
309 if not sslsocket.cipher():
316 if not sslsocket.cipher():
310 raise error.Abort(_('ssl connection failed'))
317 raise error.Abort(_('ssl connection failed'))
311
318
312 sslsocket._hgstate = {
319 sslsocket._hgstate = {
313 'caloaded': caloaded,
320 'caloaded': caloaded,
314 'hostname': serverhostname,
321 'hostname': serverhostname,
315 'settings': settings,
322 'settings': settings,
316 'ui': ui,
323 'ui': ui,
317 }
324 }
318
325
319 return sslsocket
326 return sslsocket
320
327
321 class wildcarderror(Exception):
328 class wildcarderror(Exception):
322 """Represents an error parsing wildcards in DNS name."""
329 """Represents an error parsing wildcards in DNS name."""
323
330
324 def _dnsnamematch(dn, hostname, maxwildcards=1):
331 def _dnsnamematch(dn, hostname, maxwildcards=1):
325 """Match DNS names according RFC 6125 section 6.4.3.
332 """Match DNS names according RFC 6125 section 6.4.3.
326
333
327 This code is effectively copied from CPython's ssl._dnsname_match.
334 This code is effectively copied from CPython's ssl._dnsname_match.
328
335
329 Returns a bool indicating whether the expected hostname matches
336 Returns a bool indicating whether the expected hostname matches
330 the value in ``dn``.
337 the value in ``dn``.
331 """
338 """
332 pats = []
339 pats = []
333 if not dn:
340 if not dn:
334 return False
341 return False
335
342
336 pieces = dn.split(r'.')
343 pieces = dn.split(r'.')
337 leftmost = pieces[0]
344 leftmost = pieces[0]
338 remainder = pieces[1:]
345 remainder = pieces[1:]
339 wildcards = leftmost.count('*')
346 wildcards = leftmost.count('*')
340 if wildcards > maxwildcards:
347 if wildcards > maxwildcards:
341 raise wildcarderror(
348 raise wildcarderror(
342 _('too many wildcards in certificate DNS name: %s') % dn)
349 _('too many wildcards in certificate DNS name: %s') % dn)
343
350
344 # speed up common case w/o wildcards
351 # speed up common case w/o wildcards
345 if not wildcards:
352 if not wildcards:
346 return dn.lower() == hostname.lower()
353 return dn.lower() == hostname.lower()
347
354
348 # RFC 6125, section 6.4.3, subitem 1.
355 # RFC 6125, section 6.4.3, subitem 1.
349 # The client SHOULD NOT attempt to match a presented identifier in which
356 # The client SHOULD NOT attempt to match a presented identifier in which
350 # the wildcard character comprises a label other than the left-most label.
357 # the wildcard character comprises a label other than the left-most label.
351 if leftmost == '*':
358 if leftmost == '*':
352 # When '*' is a fragment by itself, it matches a non-empty dotless
359 # When '*' is a fragment by itself, it matches a non-empty dotless
353 # fragment.
360 # fragment.
354 pats.append('[^.]+')
361 pats.append('[^.]+')
355 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
362 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
356 # RFC 6125, section 6.4.3, subitem 3.
363 # RFC 6125, section 6.4.3, subitem 3.
357 # The client SHOULD NOT attempt to match a presented identifier
364 # The client SHOULD NOT attempt to match a presented identifier
358 # where the wildcard character is embedded within an A-label or
365 # where the wildcard character is embedded within an A-label or
359 # U-label of an internationalized domain name.
366 # U-label of an internationalized domain name.
360 pats.append(re.escape(leftmost))
367 pats.append(re.escape(leftmost))
361 else:
368 else:
362 # Otherwise, '*' matches any dotless string, e.g. www*
369 # Otherwise, '*' matches any dotless string, e.g. www*
363 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
370 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
364
371
365 # add the remaining fragments, ignore any wildcards
372 # add the remaining fragments, ignore any wildcards
366 for frag in remainder:
373 for frag in remainder:
367 pats.append(re.escape(frag))
374 pats.append(re.escape(frag))
368
375
369 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
376 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
370 return pat.match(hostname) is not None
377 return pat.match(hostname) is not None
371
378
372 def _verifycert(cert, hostname):
379 def _verifycert(cert, hostname):
373 '''Verify that cert (in socket.getpeercert() format) matches hostname.
380 '''Verify that cert (in socket.getpeercert() format) matches hostname.
374 CRLs is not handled.
381 CRLs is not handled.
375
382
376 Returns error message if any problems are found and None on success.
383 Returns error message if any problems are found and None on success.
377 '''
384 '''
378 if not cert:
385 if not cert:
379 return _('no certificate received')
386 return _('no certificate received')
380
387
381 dnsnames = []
388 dnsnames = []
382 san = cert.get('subjectAltName', [])
389 san = cert.get('subjectAltName', [])
383 for key, value in san:
390 for key, value in san:
384 if key == 'DNS':
391 if key == 'DNS':
385 try:
392 try:
386 if _dnsnamematch(value, hostname):
393 if _dnsnamematch(value, hostname):
387 return
394 return
388 except wildcarderror as e:
395 except wildcarderror as e:
389 return e.args[0]
396 return e.args[0]
390
397
391 dnsnames.append(value)
398 dnsnames.append(value)
392
399
393 if not dnsnames:
400 if not dnsnames:
394 # The subject is only checked when there is no DNS in subjectAltName.
401 # The subject is only checked when there is no DNS in subjectAltName.
395 for sub in cert.get('subject', []):
402 for sub in cert.get('subject', []):
396 for key, value in sub:
403 for key, value in sub:
397 # According to RFC 2818 the most specific Common Name must
404 # According to RFC 2818 the most specific Common Name must
398 # be used.
405 # be used.
399 if key == 'commonName':
406 if key == 'commonName':
400 # 'subject' entries are unicide.
407 # 'subject' entries are unicide.
401 try:
408 try:
402 value = value.encode('ascii')
409 value = value.encode('ascii')
403 except UnicodeEncodeError:
410 except UnicodeEncodeError:
404 return _('IDN in certificate not supported')
411 return _('IDN in certificate not supported')
405
412
406 try:
413 try:
407 if _dnsnamematch(value, hostname):
414 if _dnsnamematch(value, hostname):
408 return
415 return
409 except wildcarderror as e:
416 except wildcarderror as e:
410 return e.args[0]
417 return e.args[0]
411
418
412 dnsnames.append(value)
419 dnsnames.append(value)
413
420
414 if len(dnsnames) > 1:
421 if len(dnsnames) > 1:
415 return _('certificate is for %s') % ', '.join(dnsnames)
422 return _('certificate is for %s') % ', '.join(dnsnames)
416 elif len(dnsnames) == 1:
423 elif len(dnsnames) == 1:
417 return _('certificate is for %s') % dnsnames[0]
424 return _('certificate is for %s') % dnsnames[0]
418 else:
425 else:
419 return _('no commonName or subjectAltName found in certificate')
426 return _('no commonName or subjectAltName found in certificate')
420
427
421 def _plainapplepython():
428 def _plainapplepython():
422 """return true if this seems to be a pure Apple Python that
429 """return true if this seems to be a pure Apple Python that
423 * is unfrozen and presumably has the whole mercurial module in the file
430 * is unfrozen and presumably has the whole mercurial module in the file
424 system
431 system
425 * presumably is an Apple Python that uses Apple OpenSSL which has patches
432 * presumably is an Apple Python that uses Apple OpenSSL which has patches
426 for using system certificate store CAs in addition to the provided
433 for using system certificate store CAs in addition to the provided
427 cacerts file
434 cacerts file
428 """
435 """
429 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
436 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
430 return False
437 return False
431 exe = os.path.realpath(sys.executable).lower()
438 exe = os.path.realpath(sys.executable).lower()
432 return (exe.startswith('/usr/bin/python') or
439 return (exe.startswith('/usr/bin/python') or
433 exe.startswith('/system/library/frameworks/python.framework/'))
440 exe.startswith('/system/library/frameworks/python.framework/'))
434
441
435 _systemcacertpaths = [
442 _systemcacertpaths = [
436 # RHEL, CentOS, and Fedora
443 # RHEL, CentOS, and Fedora
437 '/etc/pki/tls/certs/ca-bundle.trust.crt',
444 '/etc/pki/tls/certs/ca-bundle.trust.crt',
438 # Debian, Ubuntu, Gentoo
445 # Debian, Ubuntu, Gentoo
439 '/etc/ssl/certs/ca-certificates.crt',
446 '/etc/ssl/certs/ca-certificates.crt',
440 ]
447 ]
441
448
442 def _defaultcacerts(ui):
449 def _defaultcacerts(ui):
443 """return path to default CA certificates or None.
450 """return path to default CA certificates or None.
444
451
445 It is assumed this function is called when the returned certificates
452 It is assumed this function is called when the returned certificates
446 file will actually be used to validate connections. Therefore this
453 file will actually be used to validate connections. Therefore this
447 function may print warnings or debug messages assuming this usage.
454 function may print warnings or debug messages assuming this usage.
448
455
449 We don't print a message when the Python is able to load default
456 We don't print a message when the Python is able to load default
450 CA certs because this scenario is detected at socket connect time.
457 CA certs because this scenario is detected at socket connect time.
451 """
458 """
452 # The "certifi" Python package provides certificates. If it is installed,
459 # The "certifi" Python package provides certificates. If it is installed,
453 # assume the user intends it to be used and use it.
460 # assume the user intends it to be used and use it.
454 try:
461 try:
455 import certifi
462 import certifi
456 certs = certifi.where()
463 certs = certifi.where()
457 ui.debug('using ca certificates from certifi\n')
464 ui.debug('using ca certificates from certifi\n')
458 return certs
465 return certs
459 except ImportError:
466 except ImportError:
460 pass
467 pass
461
468
462 # On Windows, only the modern ssl module is capable of loading the system
469 # On Windows, only the modern ssl module is capable of loading the system
463 # CA certificates. If we're not capable of doing that, emit a warning
470 # CA certificates. If we're not capable of doing that, emit a warning
464 # because we'll get a certificate verification error later and the lack
471 # because we'll get a certificate verification error later and the lack
465 # of loaded CA certificates will be the reason why.
472 # of loaded CA certificates will be the reason why.
466 # Assertion: this code is only called if certificates are being verified.
473 # Assertion: this code is only called if certificates are being verified.
467 if os.name == 'nt':
474 if os.name == 'nt':
468 if not _canloaddefaultcerts:
475 if not _canloaddefaultcerts:
469 ui.warn(_('(unable to load Windows CA certificates; see '
476 ui.warn(_('(unable to load Windows CA certificates; see '
470 'https://mercurial-scm.org/wiki/SecureConnections for '
477 'https://mercurial-scm.org/wiki/SecureConnections for '
471 'how to configure Mercurial to avoid this message)\n'))
478 'how to configure Mercurial to avoid this message)\n'))
472
479
473 return None
480 return None
474
481
475 # Apple's OpenSSL has patches that allow a specially constructed certificate
482 # Apple's OpenSSL has patches that allow a specially constructed certificate
476 # to load the system CA store. If we're running on Apple Python, use this
483 # to load the system CA store. If we're running on Apple Python, use this
477 # trick.
484 # trick.
478 if _plainapplepython():
485 if _plainapplepython():
479 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
486 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
480 if os.path.exists(dummycert):
487 if os.path.exists(dummycert):
481 return dummycert
488 return dummycert
482
489
483 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
490 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
484 # load system certs, we're out of luck.
491 # load system certs, we're out of luck.
485 if sys.platform == 'darwin':
492 if sys.platform == 'darwin':
486 # FUTURE Consider looking for Homebrew or MacPorts installed certs
493 # FUTURE Consider looking for Homebrew or MacPorts installed certs
487 # files. Also consider exporting the keychain certs to a file during
494 # files. Also consider exporting the keychain certs to a file during
488 # Mercurial install.
495 # Mercurial install.
489 if not _canloaddefaultcerts:
496 if not _canloaddefaultcerts:
490 ui.warn(_('(unable to load CA certificates; see '
497 ui.warn(_('(unable to load CA certificates; see '
491 'https://mercurial-scm.org/wiki/SecureConnections for '
498 'https://mercurial-scm.org/wiki/SecureConnections for '
492 'how to configure Mercurial to avoid this message)\n'))
499 'how to configure Mercurial to avoid this message)\n'))
493 return None
500 return None
494
501
495 # Try to find CA certificates in well-known locations. We print a warning
502 # Try to find CA certificates in well-known locations. We print a warning
496 # when using a found file because we don't want too much silent magic
503 # when using a found file because we don't want too much silent magic
497 # for security settings. The expectation is that proper Mercurial
504 # for security settings. The expectation is that proper Mercurial
498 # installs will have the CA certs path defined at install time and the
505 # installs will have the CA certs path defined at install time and the
499 # installer/packager will make an appropriate decision on the user's
506 # installer/packager will make an appropriate decision on the user's
500 # behalf. We only get here and perform this setting as a feature of
507 # behalf. We only get here and perform this setting as a feature of
501 # last resort.
508 # last resort.
502 if not _canloaddefaultcerts:
509 if not _canloaddefaultcerts:
503 for path in _systemcacertpaths:
510 for path in _systemcacertpaths:
504 if os.path.isfile(path):
511 if os.path.isfile(path):
505 ui.warn(_('(using CA certificates from %s; if you see this '
512 ui.warn(_('(using CA certificates from %s; if you see this '
506 'message, your Mercurial install is not properly '
513 'message, your Mercurial install is not properly '
507 'configured; see '
514 'configured; see '
508 'https://mercurial-scm.org/wiki/SecureConnections '
515 'https://mercurial-scm.org/wiki/SecureConnections '
509 'for how to configure Mercurial to avoid this '
516 'for how to configure Mercurial to avoid this '
510 'message)\n') % path)
517 'message)\n') % path)
511 return path
518 return path
512
519
513 ui.warn(_('(unable to load CA certificates; see '
520 ui.warn(_('(unable to load CA certificates; see '
514 'https://mercurial-scm.org/wiki/SecureConnections for '
521 'https://mercurial-scm.org/wiki/SecureConnections for '
515 'how to configure Mercurial to avoid this message)\n'))
522 'how to configure Mercurial to avoid this message)\n'))
516
523
517 return None
524 return None
518
525
519 def validatesocket(sock):
526 def validatesocket(sock):
520 """Validate a socket meets security requiremnets.
527 """Validate a socket meets security requiremnets.
521
528
522 The passed socket must have been created with ``wrapsocket()``.
529 The passed socket must have been created with ``wrapsocket()``.
523 """
530 """
524 host = sock._hgstate['hostname']
531 host = sock._hgstate['hostname']
525 ui = sock._hgstate['ui']
532 ui = sock._hgstate['ui']
526 settings = sock._hgstate['settings']
533 settings = sock._hgstate['settings']
527
534
528 try:
535 try:
529 peercert = sock.getpeercert(True)
536 peercert = sock.getpeercert(True)
530 peercert2 = sock.getpeercert()
537 peercert2 = sock.getpeercert()
531 except AttributeError:
538 except AttributeError:
532 raise error.Abort(_('%s ssl connection error') % host)
539 raise error.Abort(_('%s ssl connection error') % host)
533
540
534 if not peercert:
541 if not peercert:
535 raise error.Abort(_('%s certificate error: '
542 raise error.Abort(_('%s certificate error: '
536 'no certificate received') % host)
543 'no certificate received') % host)
537
544
538 if settings['disablecertverification']:
545 if settings['disablecertverification']:
539 # We don't print the certificate fingerprint because it shouldn't
546 # We don't print the certificate fingerprint because it shouldn't
540 # be necessary: if the user requested certificate verification be
547 # be necessary: if the user requested certificate verification be
541 # disabled, they presumably already saw a message about the inability
548 # disabled, they presumably already saw a message about the inability
542 # to verify the certificate and this message would have printed the
549 # to verify the certificate and this message would have printed the
543 # fingerprint. So printing the fingerprint here adds little to no
550 # fingerprint. So printing the fingerprint here adds little to no
544 # value.
551 # value.
545 ui.warn(_('warning: connection security to %s is disabled per current '
552 ui.warn(_('warning: connection security to %s is disabled per current '
546 'settings; communication is susceptible to eavesdropping '
553 'settings; communication is susceptible to eavesdropping '
547 'and tampering\n') % host)
554 'and tampering\n') % host)
548 return
555 return
549
556
550 # If a certificate fingerprint is pinned, use it and only it to
557 # If a certificate fingerprint is pinned, use it and only it to
551 # validate the remote cert.
558 # validate the remote cert.
552 peerfingerprints = {
559 peerfingerprints = {
553 'sha1': hashlib.sha1(peercert).hexdigest(),
560 'sha1': hashlib.sha1(peercert).hexdigest(),
554 'sha256': hashlib.sha256(peercert).hexdigest(),
561 'sha256': hashlib.sha256(peercert).hexdigest(),
555 'sha512': hashlib.sha512(peercert).hexdigest(),
562 'sha512': hashlib.sha512(peercert).hexdigest(),
556 }
563 }
557
564
558 def fmtfingerprint(s):
565 def fmtfingerprint(s):
559 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
566 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
560
567
561 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
568 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
562
569
563 if settings['certfingerprints']:
570 if settings['certfingerprints']:
564 for hash, fingerprint in settings['certfingerprints']:
571 for hash, fingerprint in settings['certfingerprints']:
565 if peerfingerprints[hash].lower() == fingerprint:
572 if peerfingerprints[hash].lower() == fingerprint:
566 ui.debug('%s certificate matched fingerprint %s:%s\n' %
573 ui.debug('%s certificate matched fingerprint %s:%s\n' %
567 (host, hash, fmtfingerprint(fingerprint)))
574 (host, hash, fmtfingerprint(fingerprint)))
568 return
575 return
569
576
570 # Pinned fingerprint didn't match. This is a fatal error.
577 # Pinned fingerprint didn't match. This is a fatal error.
571 if settings['legacyfingerprint']:
578 if settings['legacyfingerprint']:
572 section = 'hostfingerprint'
579 section = 'hostfingerprint'
573 nice = fmtfingerprint(peerfingerprints['sha1'])
580 nice = fmtfingerprint(peerfingerprints['sha1'])
574 else:
581 else:
575 section = 'hostsecurity'
582 section = 'hostsecurity'
576 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
583 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
577 raise error.Abort(_('certificate for %s has unexpected '
584 raise error.Abort(_('certificate for %s has unexpected '
578 'fingerprint %s') % (host, nice),
585 'fingerprint %s') % (host, nice),
579 hint=_('check %s configuration') % section)
586 hint=_('check %s configuration') % section)
580
587
581 # Security is enabled but no CAs are loaded. We can't establish trust
588 # Security is enabled but no CAs are loaded. We can't establish trust
582 # for the cert so abort.
589 # for the cert so abort.
583 if not sock._hgstate['caloaded']:
590 if not sock._hgstate['caloaded']:
584 raise error.Abort(
591 raise error.Abort(
585 _('unable to verify security of %s (no loaded CA certificates); '
592 _('unable to verify security of %s (no loaded CA certificates); '
586 'refusing to connect') % host,
593 'refusing to connect') % host,
587 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
594 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
588 'how to configure Mercurial to avoid this error or set '
595 'how to configure Mercurial to avoid this error or set '
589 'hostsecurity.%s:fingerprints=%s to trust this server') %
596 'hostsecurity.%s:fingerprints=%s to trust this server') %
590 (host, nicefingerprint))
597 (host, nicefingerprint))
591
598
592 msg = _verifycert(peercert2, host)
599 msg = _verifycert(peercert2, host)
593 if msg:
600 if msg:
594 raise error.Abort(_('%s certificate error: %s') % (host, msg),
601 raise error.Abort(_('%s certificate error: %s') % (host, msg),
595 hint=_('set hostsecurity.%s:certfingerprints=%s '
602 hint=_('set hostsecurity.%s:certfingerprints=%s '
596 'config setting or use --insecure to connect '
603 'config setting or use --insecure to connect '
597 'insecurely') %
604 'insecurely') %
598 (host, nicefingerprint))
605 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now