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