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