##// END OF EJS Templates
sslutil: document and slightly refactor validation logic...
Gregory Szorc -
r28850:3819c349 default
parent child Browse files
Show More
@@ -1,314 +1,320 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 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 wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
109 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
110 ca_certs=None, serverhostname=None):
110 ca_certs=None, serverhostname=None):
111 """Add SSL/TLS to a socket.
111 """Add SSL/TLS to a socket.
112
112
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
113 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
114 choices based on what security options are available.
114 choices based on what security options are available.
115
115
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
116 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
117 the following additional arguments:
117 the following additional arguments:
118
118
119 * serverhostname - The expected hostname of the remote server. If the
119 * serverhostname - The expected hostname of the remote server. If the
120 server (and client) support SNI, this tells the server which certificate
120 server (and client) support SNI, this tells the server which certificate
121 to use.
121 to use.
122 """
122 """
123 if not serverhostname:
123 if not serverhostname:
124 raise error.Abort('serverhostname argument required')
124 raise error.Abort('serverhostname argument required')
125
125
126 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
126 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
127 # that both ends support, including TLS protocols. On legacy stacks,
127 # that both ends support, including TLS protocols. On legacy stacks,
128 # the highest it likely goes in TLS 1.0. On modern stacks, it can
128 # the highest it likely goes in TLS 1.0. On modern stacks, it can
129 # support TLS 1.2.
129 # support TLS 1.2.
130 #
130 #
131 # The PROTOCOL_TLSv* constants select a specific TLS version
131 # The PROTOCOL_TLSv* constants select a specific TLS version
132 # only (as opposed to multiple versions). So the method for
132 # only (as opposed to multiple versions). So the method for
133 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
133 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
134 # disable protocols via SSLContext.options and OP_NO_* constants.
134 # disable protocols via SSLContext.options and OP_NO_* constants.
135 # However, SSLContext.options doesn't work unless we have the
135 # However, SSLContext.options doesn't work unless we have the
136 # full/real SSLContext available to us.
136 # full/real SSLContext available to us.
137 #
137 #
138 # SSLv2 and SSLv3 are broken. We ban them outright.
138 # SSLv2 and SSLv3 are broken. We ban them outright.
139 if modernssl:
139 if modernssl:
140 protocol = ssl.PROTOCOL_SSLv23
140 protocol = ssl.PROTOCOL_SSLv23
141 else:
141 else:
142 protocol = ssl.PROTOCOL_TLSv1
142 protocol = ssl.PROTOCOL_TLSv1
143
143
144 # TODO use ssl.create_default_context() on modernssl.
144 # TODO use ssl.create_default_context() on modernssl.
145 sslcontext = SSLContext(protocol)
145 sslcontext = SSLContext(protocol)
146
146
147 # This is a no-op on old Python.
147 # This is a no-op on old Python.
148 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
148 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
149
149
150 # This still works on our fake SSLContext.
150 # This still works on our fake SSLContext.
151 sslcontext.verify_mode = cert_reqs
151 sslcontext.verify_mode = cert_reqs
152
152
153 if certfile is not None:
153 if certfile is not None:
154 def password():
154 def password():
155 f = keyfile or certfile
155 f = keyfile or certfile
156 return ui.getpass(_('passphrase for %s: ') % f, '')
156 return ui.getpass(_('passphrase for %s: ') % f, '')
157 sslcontext.load_cert_chain(certfile, keyfile, password)
157 sslcontext.load_cert_chain(certfile, keyfile, password)
158
158
159 if ca_certs is not None:
159 if ca_certs is not None:
160 sslcontext.load_verify_locations(cafile=ca_certs)
160 sslcontext.load_verify_locations(cafile=ca_certs)
161 else:
161 else:
162 # This is a no-op on old Python.
162 # This is a no-op on old Python.
163 sslcontext.load_default_certs()
163 sslcontext.load_default_certs()
164
164
165 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
165 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
166 # check if wrap_socket failed silently because socket had been
166 # check if wrap_socket failed silently because socket had been
167 # closed
167 # closed
168 # - see http://bugs.python.org/issue13721
168 # - see http://bugs.python.org/issue13721
169 if not sslsocket.cipher():
169 if not sslsocket.cipher():
170 raise error.Abort(_('ssl connection failed'))
170 raise error.Abort(_('ssl connection failed'))
171 return sslsocket
171 return sslsocket
172
172
173 def _verifycert(cert, hostname):
173 def _verifycert(cert, hostname):
174 '''Verify that cert (in socket.getpeercert() format) matches hostname.
174 '''Verify that cert (in socket.getpeercert() format) matches hostname.
175 CRLs is not handled.
175 CRLs is not handled.
176
176
177 Returns error message if any problems are found and None on success.
177 Returns error message if any problems are found and None on success.
178 '''
178 '''
179 if not cert:
179 if not cert:
180 return _('no certificate received')
180 return _('no certificate received')
181 dnsname = hostname.lower()
181 dnsname = hostname.lower()
182 def matchdnsname(certname):
182 def matchdnsname(certname):
183 return (certname == dnsname or
183 return (certname == dnsname or
184 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
184 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
185
185
186 san = cert.get('subjectAltName', [])
186 san = cert.get('subjectAltName', [])
187 if san:
187 if san:
188 certnames = [value.lower() for key, value in san if key == 'DNS']
188 certnames = [value.lower() for key, value in san if key == 'DNS']
189 for name in certnames:
189 for name in certnames:
190 if matchdnsname(name):
190 if matchdnsname(name):
191 return None
191 return None
192 if certnames:
192 if certnames:
193 return _('certificate is for %s') % ', '.join(certnames)
193 return _('certificate is for %s') % ', '.join(certnames)
194
194
195 # subject is only checked when subjectAltName is empty
195 # subject is only checked when subjectAltName is empty
196 for s in cert.get('subject', []):
196 for s in cert.get('subject', []):
197 key, value = s[0]
197 key, value = s[0]
198 if key == 'commonName':
198 if key == 'commonName':
199 try:
199 try:
200 # 'subject' entries are unicode
200 # 'subject' entries are unicode
201 certname = value.lower().encode('ascii')
201 certname = value.lower().encode('ascii')
202 except UnicodeEncodeError:
202 except UnicodeEncodeError:
203 return _('IDN in certificate not supported')
203 return _('IDN in certificate not supported')
204 if matchdnsname(certname):
204 if matchdnsname(certname):
205 return None
205 return None
206 return _('certificate is for %s') % certname
206 return _('certificate is for %s') % certname
207 return _('no commonName or subjectAltName found in certificate')
207 return _('no commonName or subjectAltName found in certificate')
208
208
209
209
210 # CERT_REQUIRED means fetch the cert from the server all the time AND
210 # CERT_REQUIRED means fetch the cert from the server all the time AND
211 # validate it against the CA store provided in web.cacerts.
211 # validate it against the CA store provided in web.cacerts.
212
212
213 def _plainapplepython():
213 def _plainapplepython():
214 """return true if this seems to be a pure Apple Python that
214 """return true if this seems to be a pure Apple Python that
215 * is unfrozen and presumably has the whole mercurial module in the file
215 * is unfrozen and presumably has the whole mercurial module in the file
216 system
216 system
217 * presumably is an Apple Python that uses Apple OpenSSL which has patches
217 * presumably is an Apple Python that uses Apple OpenSSL which has patches
218 for using system certificate store CAs in addition to the provided
218 for using system certificate store CAs in addition to the provided
219 cacerts file
219 cacerts file
220 """
220 """
221 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
221 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
222 return False
222 return False
223 exe = os.path.realpath(sys.executable).lower()
223 exe = os.path.realpath(sys.executable).lower()
224 return (exe.startswith('/usr/bin/python') or
224 return (exe.startswith('/usr/bin/python') or
225 exe.startswith('/system/library/frameworks/python.framework/'))
225 exe.startswith('/system/library/frameworks/python.framework/'))
226
226
227 def _defaultcacerts():
227 def _defaultcacerts():
228 """return path to CA certificates; None for system's store; ! to disable"""
228 """return path to CA certificates; None for system's store; ! to disable"""
229 if _plainapplepython():
229 if _plainapplepython():
230 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
230 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
231 if os.path.exists(dummycert):
231 if os.path.exists(dummycert):
232 return dummycert
232 return dummycert
233 if _canloaddefaultcerts:
233 if _canloaddefaultcerts:
234 return None
234 return None
235 return '!'
235 return '!'
236
236
237 def sslkwargs(ui, host):
237 def sslkwargs(ui, host):
238 kws = {'ui': ui}
238 kws = {'ui': ui}
239 hostfingerprint = ui.config('hostfingerprints', host)
239 hostfingerprint = ui.config('hostfingerprints', host)
240 if hostfingerprint:
240 if hostfingerprint:
241 return kws
241 return kws
242 cacerts = ui.config('web', 'cacerts')
242 cacerts = ui.config('web', 'cacerts')
243 if cacerts == '!':
243 if cacerts == '!':
244 pass
244 pass
245 elif cacerts:
245 elif cacerts:
246 cacerts = util.expandpath(cacerts)
246 cacerts = util.expandpath(cacerts)
247 if not os.path.exists(cacerts):
247 if not os.path.exists(cacerts):
248 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
248 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
249 else:
249 else:
250 cacerts = _defaultcacerts()
250 cacerts = _defaultcacerts()
251 if cacerts and cacerts != '!':
251 if cacerts and cacerts != '!':
252 ui.debug('using %s to enable OS X system CA\n' % cacerts)
252 ui.debug('using %s to enable OS X system CA\n' % cacerts)
253 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
253 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
254 if cacerts != '!':
254 if cacerts != '!':
255 kws.update({'ca_certs': cacerts,
255 kws.update({'ca_certs': cacerts,
256 'cert_reqs': ssl.CERT_REQUIRED,
256 'cert_reqs': ssl.CERT_REQUIRED,
257 })
257 })
258 return kws
258 return kws
259
259
260 class validator(object):
260 class validator(object):
261 def __init__(self, ui, host):
261 def __init__(self, ui, host):
262 self.ui = ui
262 self.ui = ui
263 self.host = host
263 self.host = host
264
264
265 def __call__(self, sock, strict=False):
265 def __call__(self, sock, strict=False):
266 host = self.host
266 host = self.host
267 cacerts = self.ui.config('web', 'cacerts')
268 hostfingerprints = self.ui.configlist('hostfingerprints', host)
269
267
270 if not sock.cipher(): # work around http://bugs.python.org/issue13721
268 if not sock.cipher(): # work around http://bugs.python.org/issue13721
271 raise error.Abort(_('%s ssl connection error') % host)
269 raise error.Abort(_('%s ssl connection error') % host)
272 try:
270 try:
273 peercert = sock.getpeercert(True)
271 peercert = sock.getpeercert(True)
274 peercert2 = sock.getpeercert()
272 peercert2 = sock.getpeercert()
275 except AttributeError:
273 except AttributeError:
276 raise error.Abort(_('%s ssl connection error') % host)
274 raise error.Abort(_('%s ssl connection error') % host)
277
275
278 if not peercert:
276 if not peercert:
279 raise error.Abort(_('%s certificate error: '
277 raise error.Abort(_('%s certificate error: '
280 'no certificate received') % host)
278 'no certificate received') % host)
279
280 # If a certificate fingerprint is pinned, use it and only it to
281 # validate the remote cert.
282 hostfingerprints = self.ui.configlist('hostfingerprints', host)
281 peerfingerprint = util.sha1(peercert).hexdigest()
283 peerfingerprint = util.sha1(peercert).hexdigest()
282 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
284 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
283 for x in xrange(0, len(peerfingerprint), 2)])
285 for x in xrange(0, len(peerfingerprint), 2)])
284 if hostfingerprints:
286 if hostfingerprints:
285 fingerprintmatch = False
287 fingerprintmatch = False
286 for hostfingerprint in hostfingerprints:
288 for hostfingerprint in hostfingerprints:
287 if peerfingerprint.lower() == \
289 if peerfingerprint.lower() == \
288 hostfingerprint.replace(':', '').lower():
290 hostfingerprint.replace(':', '').lower():
289 fingerprintmatch = True
291 fingerprintmatch = True
290 break
292 break
291 if not fingerprintmatch:
293 if not fingerprintmatch:
292 raise error.Abort(_('certificate for %s has unexpected '
294 raise error.Abort(_('certificate for %s has unexpected '
293 'fingerprint %s') % (host, nicefingerprint),
295 'fingerprint %s') % (host, nicefingerprint),
294 hint=_('check hostfingerprint configuration'))
296 hint=_('check hostfingerprint configuration'))
295 self.ui.debug('%s certificate matched fingerprint %s\n' %
297 self.ui.debug('%s certificate matched fingerprint %s\n' %
296 (host, nicefingerprint))
298 (host, nicefingerprint))
297 elif cacerts != '!':
299 return
300
301 # No pinned fingerprint. Establish trust by looking at the CAs.
302 cacerts = self.ui.config('web', 'cacerts')
303 if cacerts != '!':
298 msg = _verifycert(peercert2, host)
304 msg = _verifycert(peercert2, host)
299 if msg:
305 if msg:
300 raise error.Abort(_('%s certificate error: %s') % (host, msg),
306 raise error.Abort(_('%s certificate error: %s') % (host, msg),
301 hint=_('configure hostfingerprint %s or use '
307 hint=_('configure hostfingerprint %s or use '
302 '--insecure to connect insecurely') %
308 '--insecure to connect insecurely') %
303 nicefingerprint)
309 nicefingerprint)
304 self.ui.debug('%s certificate successfully verified\n' % host)
310 self.ui.debug('%s certificate successfully verified\n' % host)
305 elif strict:
311 elif strict:
306 raise error.Abort(_('%s certificate with fingerprint %s not '
312 raise error.Abort(_('%s certificate with fingerprint %s not '
307 'verified') % (host, nicefingerprint),
313 'verified') % (host, nicefingerprint),
308 hint=_('check hostfingerprints or web.cacerts '
314 hint=_('check hostfingerprints or web.cacerts '
309 'config setting'))
315 'config setting'))
310 else:
316 else:
311 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
317 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
312 'verified (check hostfingerprints or web.cacerts '
318 'verified (check hostfingerprints or web.cacerts '
313 'config setting)\n') %
319 'config setting)\n') %
314 (host, nicefingerprint))
320 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now