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