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