##// END OF EJS Templates
sslutil: don't load default certificates when they aren't relevant...
Gregory Szorc -
r29447:13edc11e default
parent child Browse files
Show More
@@ -1,439 +1,441 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 ssl
14 import ssl
15 import sys
15 import sys
16
16
17 from .i18n import _
17 from .i18n import _
18 from . import (
18 from . import (
19 error,
19 error,
20 util,
20 util,
21 )
21 )
22
22
23 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
23 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
24 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
24 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
25 # all exposed via the "ssl" module.
25 # all exposed via the "ssl" module.
26 #
26 #
27 # Depending on the version of Python being used, SSL/TLS support is either
27 # Depending on the version of Python being used, SSL/TLS support is either
28 # modern/secure or legacy/insecure. Many operations in this module have
28 # modern/secure or legacy/insecure. Many operations in this module have
29 # separate code paths depending on support in Python.
29 # separate code paths depending on support in Python.
30
30
31 hassni = getattr(ssl, 'HAS_SNI', False)
31 hassni = getattr(ssl, 'HAS_SNI', False)
32
32
33 try:
33 try:
34 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
34 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
35 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
35 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
36 except AttributeError:
36 except AttributeError:
37 OP_NO_SSLv2 = 0x1000000
37 OP_NO_SSLv2 = 0x1000000
38 OP_NO_SSLv3 = 0x2000000
38 OP_NO_SSLv3 = 0x2000000
39
39
40 try:
40 try:
41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 # SSL/TLS features are available.
42 # SSL/TLS features are available.
43 SSLContext = ssl.SSLContext
43 SSLContext = ssl.SSLContext
44 modernssl = True
44 modernssl = True
45 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
45 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
46 except AttributeError:
46 except AttributeError:
47 modernssl = False
47 modernssl = False
48 _canloaddefaultcerts = False
48 _canloaddefaultcerts = False
49
49
50 # We implement SSLContext using the interface from the standard library.
50 # We implement SSLContext using the interface from the standard library.
51 class SSLContext(object):
51 class SSLContext(object):
52 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
52 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
53 _supportsciphers = sys.version_info >= (2, 7)
53 _supportsciphers = sys.version_info >= (2, 7)
54
54
55 def __init__(self, protocol):
55 def __init__(self, protocol):
56 # From the public interface of SSLContext
56 # From the public interface of SSLContext
57 self.protocol = protocol
57 self.protocol = protocol
58 self.check_hostname = False
58 self.check_hostname = False
59 self.options = 0
59 self.options = 0
60 self.verify_mode = ssl.CERT_NONE
60 self.verify_mode = ssl.CERT_NONE
61
61
62 # Used by our implementation.
62 # Used by our implementation.
63 self._certfile = None
63 self._certfile = None
64 self._keyfile = None
64 self._keyfile = None
65 self._certpassword = None
65 self._certpassword = None
66 self._cacerts = None
66 self._cacerts = None
67 self._ciphers = None
67 self._ciphers = None
68
68
69 def load_cert_chain(self, certfile, keyfile=None, password=None):
69 def load_cert_chain(self, certfile, keyfile=None, password=None):
70 self._certfile = certfile
70 self._certfile = certfile
71 self._keyfile = keyfile
71 self._keyfile = keyfile
72 self._certpassword = password
72 self._certpassword = password
73
73
74 def load_default_certs(self, purpose=None):
74 def load_default_certs(self, purpose=None):
75 pass
75 pass
76
76
77 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
77 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 if capath:
78 if capath:
79 raise error.Abort(_('capath not supported'))
79 raise error.Abort(_('capath not supported'))
80 if cadata:
80 if cadata:
81 raise error.Abort(_('cadata not supported'))
81 raise error.Abort(_('cadata not supported'))
82
82
83 self._cacerts = cafile
83 self._cacerts = cafile
84
84
85 def set_ciphers(self, ciphers):
85 def set_ciphers(self, ciphers):
86 if not self._supportsciphers:
86 if not self._supportsciphers:
87 raise error.Abort(_('setting ciphers not supported'))
87 raise error.Abort(_('setting ciphers not supported'))
88
88
89 self._ciphers = ciphers
89 self._ciphers = ciphers
90
90
91 def wrap_socket(self, socket, server_hostname=None, server_side=False):
91 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 # server_hostname is unique to SSLContext.wrap_socket and is used
92 # server_hostname is unique to SSLContext.wrap_socket and is used
93 # for SNI in that context. So there's nothing for us to do with it
93 # for SNI in that context. So there's nothing for us to do with it
94 # in this legacy code since we don't support SNI.
94 # in this legacy code since we don't support SNI.
95
95
96 args = {
96 args = {
97 'keyfile': self._keyfile,
97 'keyfile': self._keyfile,
98 'certfile': self._certfile,
98 'certfile': self._certfile,
99 'server_side': server_side,
99 'server_side': server_side,
100 'cert_reqs': self.verify_mode,
100 'cert_reqs': self.verify_mode,
101 'ssl_version': self.protocol,
101 'ssl_version': self.protocol,
102 'ca_certs': self._cacerts,
102 'ca_certs': self._cacerts,
103 }
103 }
104
104
105 if self._supportsciphers:
105 if self._supportsciphers:
106 args['ciphers'] = self._ciphers
106 args['ciphers'] = self._ciphers
107
107
108 return ssl.wrap_socket(socket, **args)
108 return ssl.wrap_socket(socket, **args)
109
109
110 def _hostsettings(ui, hostname):
110 def _hostsettings(ui, hostname):
111 """Obtain security settings for a hostname.
111 """Obtain security settings for a hostname.
112
112
113 Returns a dict of settings relevant to that hostname.
113 Returns a dict of settings relevant to that hostname.
114 """
114 """
115 s = {
115 s = {
116 # Whether we should attempt to load default/available CA certs
116 # Whether we should attempt to load default/available CA certs
117 # if an explicit ``cafile`` is not defined.
117 # if an explicit ``cafile`` is not defined.
118 'allowloaddefaultcerts': True,
118 'allowloaddefaultcerts': True,
119 # List of 2-tuple of (hash algorithm, hash).
119 # List of 2-tuple of (hash algorithm, hash).
120 'certfingerprints': [],
120 'certfingerprints': [],
121 # Path to file containing concatenated CA certs. Used by
121 # Path to file containing concatenated CA certs. Used by
122 # SSLContext.load_verify_locations().
122 # SSLContext.load_verify_locations().
123 'cafile': None,
123 'cafile': None,
124 # Whether certificate verification should be disabled.
124 # Whether certificate verification should be disabled.
125 'disablecertverification': False,
125 'disablecertverification': False,
126 # Whether the legacy [hostfingerprints] section has data for this host.
126 # Whether the legacy [hostfingerprints] section has data for this host.
127 'legacyfingerprint': False,
127 'legacyfingerprint': False,
128 # ssl.CERT_* constant used by SSLContext.verify_mode.
128 # ssl.CERT_* constant used by SSLContext.verify_mode.
129 'verifymode': None,
129 'verifymode': None,
130 }
130 }
131
131
132 # Look for fingerprints in [hostsecurity] section. Value is a list
132 # Look for fingerprints in [hostsecurity] section. Value is a list
133 # of <alg>:<fingerprint> strings.
133 # of <alg>:<fingerprint> strings.
134 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
134 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
135 [])
135 [])
136 for fingerprint in fingerprints:
136 for fingerprint in fingerprints:
137 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
137 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
138 raise error.Abort(_('invalid fingerprint for %s: %s') % (
138 raise error.Abort(_('invalid fingerprint for %s: %s') % (
139 hostname, fingerprint),
139 hostname, fingerprint),
140 hint=_('must begin with "sha1:", "sha256:", '
140 hint=_('must begin with "sha1:", "sha256:", '
141 'or "sha512:"'))
141 'or "sha512:"'))
142
142
143 alg, fingerprint = fingerprint.split(':', 1)
143 alg, fingerprint = fingerprint.split(':', 1)
144 fingerprint = fingerprint.replace(':', '').lower()
144 fingerprint = fingerprint.replace(':', '').lower()
145 s['certfingerprints'].append((alg, fingerprint))
145 s['certfingerprints'].append((alg, fingerprint))
146
146
147 # Fingerprints from [hostfingerprints] are always SHA-1.
147 # Fingerprints from [hostfingerprints] are always SHA-1.
148 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
148 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
149 fingerprint = fingerprint.replace(':', '').lower()
149 fingerprint = fingerprint.replace(':', '').lower()
150 s['certfingerprints'].append(('sha1', fingerprint))
150 s['certfingerprints'].append(('sha1', fingerprint))
151 s['legacyfingerprint'] = True
151 s['legacyfingerprint'] = True
152
152
153 # If a host cert fingerprint is defined, it is the only thing that
153 # If a host cert fingerprint is defined, it is the only thing that
154 # matters. No need to validate CA certs.
154 # matters. No need to validate CA certs.
155 if s['certfingerprints']:
155 if s['certfingerprints']:
156 s['verifymode'] = ssl.CERT_NONE
156 s['verifymode'] = ssl.CERT_NONE
157 s['allowloaddefaultcerts'] = False
157
158
158 # If --insecure is used, don't take CAs into consideration.
159 # If --insecure is used, don't take CAs into consideration.
159 elif ui.insecureconnections:
160 elif ui.insecureconnections:
160 s['disablecertverification'] = True
161 s['disablecertverification'] = True
161 s['verifymode'] = ssl.CERT_NONE
162 s['verifymode'] = ssl.CERT_NONE
163 s['allowloaddefaultcerts'] = False
162
164
163 if ui.configbool('devel', 'disableloaddefaultcerts'):
165 if ui.configbool('devel', 'disableloaddefaultcerts'):
164 s['allowloaddefaultcerts'] = False
166 s['allowloaddefaultcerts'] = False
165
167
166 # If both fingerprints and a per-host ca file are specified, issue a warning
168 # If both fingerprints and a per-host ca file are specified, issue a warning
167 # because users should not be surprised about what security is or isn't
169 # because users should not be surprised about what security is or isn't
168 # being performed.
170 # being performed.
169 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
171 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
170 if s['certfingerprints'] and cafile:
172 if s['certfingerprints'] and cafile:
171 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
173 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
172 'fingerprints defined; using host fingerprints for '
174 'fingerprints defined; using host fingerprints for '
173 'verification)\n') % hostname)
175 'verification)\n') % hostname)
174
176
175 # Try to hook up CA certificate validation unless something above
177 # Try to hook up CA certificate validation unless something above
176 # makes it not necessary.
178 # makes it not necessary.
177 if s['verifymode'] is None:
179 if s['verifymode'] is None:
178 # Look at per-host ca file first.
180 # Look at per-host ca file first.
179 if cafile:
181 if cafile:
180 cafile = util.expandpath(cafile)
182 cafile = util.expandpath(cafile)
181 if not os.path.exists(cafile):
183 if not os.path.exists(cafile):
182 raise error.Abort(_('path specified by %s does not exist: %s') %
184 raise error.Abort(_('path specified by %s does not exist: %s') %
183 ('hostsecurity.%s:verifycertsfile' % hostname,
185 ('hostsecurity.%s:verifycertsfile' % hostname,
184 cafile))
186 cafile))
185 s['cafile'] = cafile
187 s['cafile'] = cafile
186 else:
188 else:
187 # Find global certificates file in config.
189 # Find global certificates file in config.
188 cafile = ui.config('web', 'cacerts')
190 cafile = ui.config('web', 'cacerts')
189
191
190 if cafile:
192 if cafile:
191 cafile = util.expandpath(cafile)
193 cafile = util.expandpath(cafile)
192 if not os.path.exists(cafile):
194 if not os.path.exists(cafile):
193 raise error.Abort(_('could not find web.cacerts: %s') %
195 raise error.Abort(_('could not find web.cacerts: %s') %
194 cafile)
196 cafile)
195 else:
197 else:
196 # No global CA certs. See if we can load defaults.
198 # No global CA certs. See if we can load defaults.
197 cafile = _defaultcacerts()
199 cafile = _defaultcacerts()
198 if cafile:
200 if cafile:
199 ui.debug('using %s to enable OS X system CA\n' % cafile)
201 ui.debug('using %s to enable OS X system CA\n' % cafile)
200
202
201 s['cafile'] = cafile
203 s['cafile'] = cafile
202
204
203 # Require certificate validation if CA certs are being loaded and
205 # Require certificate validation if CA certs are being loaded and
204 # verification hasn't been disabled above.
206 # verification hasn't been disabled above.
205 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
207 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
206 s['verifymode'] = ssl.CERT_REQUIRED
208 s['verifymode'] = ssl.CERT_REQUIRED
207 else:
209 else:
208 # At this point we don't have a fingerprint, aren't being
210 # At this point we don't have a fingerprint, aren't being
209 # explicitly insecure, and can't load CA certs. Connecting
211 # explicitly insecure, and can't load CA certs. Connecting
210 # is insecure. We allow the connection and abort during
212 # is insecure. We allow the connection and abort during
211 # validation (once we have the fingerprint to print to the
213 # validation (once we have the fingerprint to print to the
212 # user).
214 # user).
213 s['verifymode'] = ssl.CERT_NONE
215 s['verifymode'] = ssl.CERT_NONE
214
216
215 assert s['verifymode'] is not None
217 assert s['verifymode'] is not None
216
218
217 return s
219 return s
218
220
219 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
221 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
220 """Add SSL/TLS to a socket.
222 """Add SSL/TLS to a socket.
221
223
222 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
224 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
223 choices based on what security options are available.
225 choices based on what security options are available.
224
226
225 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
227 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
226 the following additional arguments:
228 the following additional arguments:
227
229
228 * serverhostname - The expected hostname of the remote server. If the
230 * serverhostname - The expected hostname of the remote server. If the
229 server (and client) support SNI, this tells the server which certificate
231 server (and client) support SNI, this tells the server which certificate
230 to use.
232 to use.
231 """
233 """
232 if not serverhostname:
234 if not serverhostname:
233 raise error.Abort(_('serverhostname argument is required'))
235 raise error.Abort(_('serverhostname argument is required'))
234
236
235 settings = _hostsettings(ui, serverhostname)
237 settings = _hostsettings(ui, serverhostname)
236
238
237 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
239 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
238 # that both ends support, including TLS protocols. On legacy stacks,
240 # that both ends support, including TLS protocols. On legacy stacks,
239 # the highest it likely goes in TLS 1.0. On modern stacks, it can
241 # the highest it likely goes in TLS 1.0. On modern stacks, it can
240 # support TLS 1.2.
242 # support TLS 1.2.
241 #
243 #
242 # The PROTOCOL_TLSv* constants select a specific TLS version
244 # The PROTOCOL_TLSv* constants select a specific TLS version
243 # only (as opposed to multiple versions). So the method for
245 # only (as opposed to multiple versions). So the method for
244 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
246 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
245 # disable protocols via SSLContext.options and OP_NO_* constants.
247 # disable protocols via SSLContext.options and OP_NO_* constants.
246 # However, SSLContext.options doesn't work unless we have the
248 # However, SSLContext.options doesn't work unless we have the
247 # full/real SSLContext available to us.
249 # full/real SSLContext available to us.
248 #
250 #
249 # SSLv2 and SSLv3 are broken. We ban them outright.
251 # SSLv2 and SSLv3 are broken. We ban them outright.
250 if modernssl:
252 if modernssl:
251 protocol = ssl.PROTOCOL_SSLv23
253 protocol = ssl.PROTOCOL_SSLv23
252 else:
254 else:
253 protocol = ssl.PROTOCOL_TLSv1
255 protocol = ssl.PROTOCOL_TLSv1
254
256
255 # TODO use ssl.create_default_context() on modernssl.
257 # TODO use ssl.create_default_context() on modernssl.
256 sslcontext = SSLContext(protocol)
258 sslcontext = SSLContext(protocol)
257
259
258 # This is a no-op on old Python.
260 # This is a no-op on old Python.
259 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
261 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
260
262
261 # This still works on our fake SSLContext.
263 # This still works on our fake SSLContext.
262 sslcontext.verify_mode = settings['verifymode']
264 sslcontext.verify_mode = settings['verifymode']
263
265
264 if certfile is not None:
266 if certfile is not None:
265 def password():
267 def password():
266 f = keyfile or certfile
268 f = keyfile or certfile
267 return ui.getpass(_('passphrase for %s: ') % f, '')
269 return ui.getpass(_('passphrase for %s: ') % f, '')
268 sslcontext.load_cert_chain(certfile, keyfile, password)
270 sslcontext.load_cert_chain(certfile, keyfile, password)
269
271
270 if settings['cafile'] is not None:
272 if settings['cafile'] is not None:
271 try:
273 try:
272 sslcontext.load_verify_locations(cafile=settings['cafile'])
274 sslcontext.load_verify_locations(cafile=settings['cafile'])
273 except ssl.SSLError as e:
275 except ssl.SSLError as e:
274 raise error.Abort(_('error loading CA file %s: %s') % (
276 raise error.Abort(_('error loading CA file %s: %s') % (
275 settings['cafile'], e.args[1]),
277 settings['cafile'], e.args[1]),
276 hint=_('file is empty or malformed?'))
278 hint=_('file is empty or malformed?'))
277 caloaded = True
279 caloaded = True
278 elif settings['allowloaddefaultcerts']:
280 elif settings['allowloaddefaultcerts']:
279 # This is a no-op on old Python.
281 # This is a no-op on old Python.
280 sslcontext.load_default_certs()
282 sslcontext.load_default_certs()
281 caloaded = True
283 caloaded = True
282 else:
284 else:
283 caloaded = False
285 caloaded = False
284
286
285 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
287 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
286 # check if wrap_socket failed silently because socket had been
288 # check if wrap_socket failed silently because socket had been
287 # closed
289 # closed
288 # - see http://bugs.python.org/issue13721
290 # - see http://bugs.python.org/issue13721
289 if not sslsocket.cipher():
291 if not sslsocket.cipher():
290 raise error.Abort(_('ssl connection failed'))
292 raise error.Abort(_('ssl connection failed'))
291
293
292 sslsocket._hgstate = {
294 sslsocket._hgstate = {
293 'caloaded': caloaded,
295 'caloaded': caloaded,
294 'hostname': serverhostname,
296 'hostname': serverhostname,
295 'settings': settings,
297 'settings': settings,
296 'ui': ui,
298 'ui': ui,
297 }
299 }
298
300
299 return sslsocket
301 return sslsocket
300
302
301 def _verifycert(cert, hostname):
303 def _verifycert(cert, hostname):
302 '''Verify that cert (in socket.getpeercert() format) matches hostname.
304 '''Verify that cert (in socket.getpeercert() format) matches hostname.
303 CRLs is not handled.
305 CRLs is not handled.
304
306
305 Returns error message if any problems are found and None on success.
307 Returns error message if any problems are found and None on success.
306 '''
308 '''
307 if not cert:
309 if not cert:
308 return _('no certificate received')
310 return _('no certificate received')
309 dnsname = hostname.lower()
311 dnsname = hostname.lower()
310 def matchdnsname(certname):
312 def matchdnsname(certname):
311 return (certname == dnsname or
313 return (certname == dnsname or
312 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
314 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
313
315
314 san = cert.get('subjectAltName', [])
316 san = cert.get('subjectAltName', [])
315 if san:
317 if san:
316 certnames = [value.lower() for key, value in san if key == 'DNS']
318 certnames = [value.lower() for key, value in san if key == 'DNS']
317 for name in certnames:
319 for name in certnames:
318 if matchdnsname(name):
320 if matchdnsname(name):
319 return None
321 return None
320 if certnames:
322 if certnames:
321 return _('certificate is for %s') % ', '.join(certnames)
323 return _('certificate is for %s') % ', '.join(certnames)
322
324
323 # subject is only checked when subjectAltName is empty
325 # subject is only checked when subjectAltName is empty
324 for s in cert.get('subject', []):
326 for s in cert.get('subject', []):
325 key, value = s[0]
327 key, value = s[0]
326 if key == 'commonName':
328 if key == 'commonName':
327 try:
329 try:
328 # 'subject' entries are unicode
330 # 'subject' entries are unicode
329 certname = value.lower().encode('ascii')
331 certname = value.lower().encode('ascii')
330 except UnicodeEncodeError:
332 except UnicodeEncodeError:
331 return _('IDN in certificate not supported')
333 return _('IDN in certificate not supported')
332 if matchdnsname(certname):
334 if matchdnsname(certname):
333 return None
335 return None
334 return _('certificate is for %s') % certname
336 return _('certificate is for %s') % certname
335 return _('no commonName or subjectAltName found in certificate')
337 return _('no commonName or subjectAltName found in certificate')
336
338
337 def _plainapplepython():
339 def _plainapplepython():
338 """return true if this seems to be a pure Apple Python that
340 """return true if this seems to be a pure Apple Python that
339 * is unfrozen and presumably has the whole mercurial module in the file
341 * is unfrozen and presumably has the whole mercurial module in the file
340 system
342 system
341 * presumably is an Apple Python that uses Apple OpenSSL which has patches
343 * presumably is an Apple Python that uses Apple OpenSSL which has patches
342 for using system certificate store CAs in addition to the provided
344 for using system certificate store CAs in addition to the provided
343 cacerts file
345 cacerts file
344 """
346 """
345 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
347 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
346 return False
348 return False
347 exe = os.path.realpath(sys.executable).lower()
349 exe = os.path.realpath(sys.executable).lower()
348 return (exe.startswith('/usr/bin/python') or
350 return (exe.startswith('/usr/bin/python') or
349 exe.startswith('/system/library/frameworks/python.framework/'))
351 exe.startswith('/system/library/frameworks/python.framework/'))
350
352
351 def _defaultcacerts():
353 def _defaultcacerts():
352 """return path to default CA certificates or None."""
354 """return path to default CA certificates or None."""
353 if _plainapplepython():
355 if _plainapplepython():
354 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
356 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
355 if os.path.exists(dummycert):
357 if os.path.exists(dummycert):
356 return dummycert
358 return dummycert
357
359
358 return None
360 return None
359
361
360 def validatesocket(sock):
362 def validatesocket(sock):
361 """Validate a socket meets security requiremnets.
363 """Validate a socket meets security requiremnets.
362
364
363 The passed socket must have been created with ``wrapsocket()``.
365 The passed socket must have been created with ``wrapsocket()``.
364 """
366 """
365 host = sock._hgstate['hostname']
367 host = sock._hgstate['hostname']
366 ui = sock._hgstate['ui']
368 ui = sock._hgstate['ui']
367 settings = sock._hgstate['settings']
369 settings = sock._hgstate['settings']
368
370
369 try:
371 try:
370 peercert = sock.getpeercert(True)
372 peercert = sock.getpeercert(True)
371 peercert2 = sock.getpeercert()
373 peercert2 = sock.getpeercert()
372 except AttributeError:
374 except AttributeError:
373 raise error.Abort(_('%s ssl connection error') % host)
375 raise error.Abort(_('%s ssl connection error') % host)
374
376
375 if not peercert:
377 if not peercert:
376 raise error.Abort(_('%s certificate error: '
378 raise error.Abort(_('%s certificate error: '
377 'no certificate received') % host)
379 'no certificate received') % host)
378
380
379 if settings['disablecertverification']:
381 if settings['disablecertverification']:
380 # We don't print the certificate fingerprint because it shouldn't
382 # We don't print the certificate fingerprint because it shouldn't
381 # be necessary: if the user requested certificate verification be
383 # be necessary: if the user requested certificate verification be
382 # disabled, they presumably already saw a message about the inability
384 # disabled, they presumably already saw a message about the inability
383 # to verify the certificate and this message would have printed the
385 # to verify the certificate and this message would have printed the
384 # fingerprint. So printing the fingerprint here adds little to no
386 # fingerprint. So printing the fingerprint here adds little to no
385 # value.
387 # value.
386 ui.warn(_('warning: connection security to %s is disabled per current '
388 ui.warn(_('warning: connection security to %s is disabled per current '
387 'settings; communication is susceptible to eavesdropping '
389 'settings; communication is susceptible to eavesdropping '
388 'and tampering\n') % host)
390 'and tampering\n') % host)
389 return
391 return
390
392
391 # If a certificate fingerprint is pinned, use it and only it to
393 # If a certificate fingerprint is pinned, use it and only it to
392 # validate the remote cert.
394 # validate the remote cert.
393 peerfingerprints = {
395 peerfingerprints = {
394 'sha1': hashlib.sha1(peercert).hexdigest(),
396 'sha1': hashlib.sha1(peercert).hexdigest(),
395 'sha256': hashlib.sha256(peercert).hexdigest(),
397 'sha256': hashlib.sha256(peercert).hexdigest(),
396 'sha512': hashlib.sha512(peercert).hexdigest(),
398 'sha512': hashlib.sha512(peercert).hexdigest(),
397 }
399 }
398
400
399 def fmtfingerprint(s):
401 def fmtfingerprint(s):
400 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
402 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
401
403
402 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
404 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
403
405
404 if settings['certfingerprints']:
406 if settings['certfingerprints']:
405 for hash, fingerprint in settings['certfingerprints']:
407 for hash, fingerprint in settings['certfingerprints']:
406 if peerfingerprints[hash].lower() == fingerprint:
408 if peerfingerprints[hash].lower() == fingerprint:
407 ui.debug('%s certificate matched fingerprint %s:%s\n' %
409 ui.debug('%s certificate matched fingerprint %s:%s\n' %
408 (host, hash, fmtfingerprint(fingerprint)))
410 (host, hash, fmtfingerprint(fingerprint)))
409 return
411 return
410
412
411 # Pinned fingerprint didn't match. This is a fatal error.
413 # Pinned fingerprint didn't match. This is a fatal error.
412 if settings['legacyfingerprint']:
414 if settings['legacyfingerprint']:
413 section = 'hostfingerprint'
415 section = 'hostfingerprint'
414 nice = fmtfingerprint(peerfingerprints['sha1'])
416 nice = fmtfingerprint(peerfingerprints['sha1'])
415 else:
417 else:
416 section = 'hostsecurity'
418 section = 'hostsecurity'
417 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
419 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
418 raise error.Abort(_('certificate for %s has unexpected '
420 raise error.Abort(_('certificate for %s has unexpected '
419 'fingerprint %s') % (host, nice),
421 'fingerprint %s') % (host, nice),
420 hint=_('check %s configuration') % section)
422 hint=_('check %s configuration') % section)
421
423
422 # Security is enabled but no CAs are loaded. We can't establish trust
424 # Security is enabled but no CAs are loaded. We can't establish trust
423 # for the cert so abort.
425 # for the cert so abort.
424 if not sock._hgstate['caloaded']:
426 if not sock._hgstate['caloaded']:
425 raise error.Abort(
427 raise error.Abort(
426 _('unable to verify security of %s (no loaded CA certificates); '
428 _('unable to verify security of %s (no loaded CA certificates); '
427 'refusing to connect') % host,
429 'refusing to connect') % host,
428 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
430 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
429 'how to configure Mercurial to avoid this error or set '
431 'how to configure Mercurial to avoid this error or set '
430 'hostsecurity.%s:fingerprints=%s to trust this server') %
432 'hostsecurity.%s:fingerprints=%s to trust this server') %
431 (host, nicefingerprint))
433 (host, nicefingerprint))
432
434
433 msg = _verifycert(peercert2, host)
435 msg = _verifycert(peercert2, host)
434 if msg:
436 if msg:
435 raise error.Abort(_('%s certificate error: %s') % (host, msg),
437 raise error.Abort(_('%s certificate error: %s') % (host, msg),
436 hint=_('set hostsecurity.%s:certfingerprints=%s '
438 hint=_('set hostsecurity.%s:certfingerprints=%s '
437 'config setting or use --insecure to connect '
439 'config setting or use --insecure to connect '
438 'insecurely') %
440 'insecurely') %
439 (host, nicefingerprint))
441 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now