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