##// END OF EJS Templates
sslutil: always use SSLContext...
Gregory Szorc -
r28651:4827d070 default
parent child Browse files
Show More
@@ -1,301 +1,299 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import os
13 13 import ssl
14 14 import sys
15 15
16 16 from .i18n import _
17 17 from . import (
18 18 error,
19 19 util,
20 20 )
21 21
22 22 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
23 23 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
24 24 # all exposed via the "ssl" module.
25 25 #
26 26 # Depending on the version of Python being used, SSL/TLS support is either
27 27 # modern/secure or legacy/insecure. Many operations in this module have
28 28 # separate code paths depending on support in Python.
29 29
30 30 hassni = getattr(ssl, 'HAS_SNI', False)
31 31
32 32 try:
33 33 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
34 34 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
35 35 except AttributeError:
36 36 OP_NO_SSLv2 = 0x1000000
37 37 OP_NO_SSLv3 = 0x2000000
38 38
39 39 try:
40 40 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
41 41 # SSL/TLS features are available.
42 42 SSLContext = ssl.SSLContext
43 43 modernssl = True
44 44 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
45 45 except AttributeError:
46 46 modernssl = False
47 47 _canloaddefaultcerts = False
48 48
49 49 # We implement SSLContext using the interface from the standard library.
50 50 class SSLContext(object):
51 51 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
52 52 _supportsciphers = sys.version_info >= (2, 7)
53 53
54 54 def __init__(self, protocol):
55 55 # From the public interface of SSLContext
56 56 self.protocol = protocol
57 57 self.check_hostname = False
58 58 self.options = 0
59 59 self.verify_mode = ssl.CERT_NONE
60 60
61 61 # Used by our implementation.
62 62 self._certfile = None
63 63 self._keyfile = None
64 64 self._certpassword = None
65 65 self._cacerts = None
66 66 self._ciphers = None
67 67
68 68 def load_cert_chain(self, certfile, keyfile=None, password=None):
69 69 self._certfile = certfile
70 70 self._keyfile = keyfile
71 71 self._certpassword = password
72 72
73 73 def load_default_certs(self, purpose=None):
74 74 pass
75 75
76 76 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
77 77 if capath:
78 78 raise error.Abort('capath not supported')
79 79 if cadata:
80 80 raise error.Abort('cadata not supported')
81 81
82 82 self._cacerts = cafile
83 83
84 84 def set_ciphers(self, ciphers):
85 85 if not self._supportsciphers:
86 86 raise error.Abort('setting ciphers not supported')
87 87
88 88 self._ciphers = ciphers
89 89
90 90 def wrap_socket(self, socket, server_hostname=None, server_side=False):
91 91 # server_hostname is unique to SSLContext.wrap_socket and is used
92 92 # for SNI in that context. So there's nothing for us to do with it
93 93 # in this legacy code since we don't support SNI.
94 94
95 95 args = {
96 96 'keyfile': self._keyfile,
97 97 'certfile': self._certfile,
98 98 'server_side': server_side,
99 99 'cert_reqs': self.verify_mode,
100 100 'ssl_version': self.protocol,
101 101 'ca_certs': self._cacerts,
102 102 }
103 103
104 104 if self._supportsciphers:
105 105 args['ciphers'] = self._ciphers
106 106
107 107 return ssl.wrap_socket(socket, **args)
108 108
109 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 110 def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
115 111 ca_certs=None, serverhostname=None):
116 # Allow any version of SSL starting with TLSv1 and
117 # up. Note that specifying TLSv1 here prohibits use of
118 # newer standards (like TLSv1_2), so this is the right way
119 # to do this. Note that in the future it'd be better to
120 # support using ssl.create_default_context(), which sets
121 # up a bunch of things in smart ways (strong ciphers,
122 # protocol versions, etc) and is upgraded by Python
123 # maintainers for us, but that breaks too many things to
124 # do it in a hurry.
125 sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
112 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
113 # that both ends support, including TLS protocols. On legacy stacks,
114 # the highest it likely goes in TLS 1.0. On modern stacks, it can
115 # support TLS 1.2.
116 #
117 # The PROTOCOL_TLSv* constants select a specific TLS version
118 # only (as opposed to multiple versions). So the method for
119 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
120 # disable protocols via SSLContext.options and OP_NO_* constants.
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 134 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
135
127 136 if certfile is not None:
128 137 def password():
129 138 f = keyfile or certfile
130 139 return ui.getpass(_('passphrase for %s: ') % f, '')
131 140 sslcontext.load_cert_chain(certfile, keyfile, password)
132 141 sslcontext.verify_mode = cert_reqs
133 142 if ca_certs is not None:
134 143 sslcontext.load_verify_locations(cafile=ca_certs)
135 elif _canloaddefaultcerts:
144 else:
145 # This is a no-op on old Python.
136 146 sslcontext.load_default_certs()
137 147
138 148 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
139 149 # check if wrap_socket failed silently because socket had been
140 150 # closed
141 151 # - see http://bugs.python.org/issue13721
142 152 if not sslsocket.cipher():
143 153 raise error.Abort(_('ssl connection failed'))
144 154 return sslsocket
145 155 except AttributeError:
146 # We don't have a modern version of the "ssl" module and are running
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
156 raise util.Abort('this should not happen')
159 157
160 158 def _verifycert(cert, hostname):
161 159 '''Verify that cert (in socket.getpeercert() format) matches hostname.
162 160 CRLs is not handled.
163 161
164 162 Returns error message if any problems are found and None on success.
165 163 '''
166 164 if not cert:
167 165 return _('no certificate received')
168 166 dnsname = hostname.lower()
169 167 def matchdnsname(certname):
170 168 return (certname == dnsname or
171 169 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
172 170
173 171 san = cert.get('subjectAltName', [])
174 172 if san:
175 173 certnames = [value.lower() for key, value in san if key == 'DNS']
176 174 for name in certnames:
177 175 if matchdnsname(name):
178 176 return None
179 177 if certnames:
180 178 return _('certificate is for %s') % ', '.join(certnames)
181 179
182 180 # subject is only checked when subjectAltName is empty
183 181 for s in cert.get('subject', []):
184 182 key, value = s[0]
185 183 if key == 'commonName':
186 184 try:
187 185 # 'subject' entries are unicode
188 186 certname = value.lower().encode('ascii')
189 187 except UnicodeEncodeError:
190 188 return _('IDN in certificate not supported')
191 189 if matchdnsname(certname):
192 190 return None
193 191 return _('certificate is for %s') % certname
194 192 return _('no commonName or subjectAltName found in certificate')
195 193
196 194
197 195 # CERT_REQUIRED means fetch the cert from the server all the time AND
198 196 # validate it against the CA store provided in web.cacerts.
199 197
200 198 def _plainapplepython():
201 199 """return true if this seems to be a pure Apple Python that
202 200 * is unfrozen and presumably has the whole mercurial module in the file
203 201 system
204 202 * presumably is an Apple Python that uses Apple OpenSSL which has patches
205 203 for using system certificate store CAs in addition to the provided
206 204 cacerts file
207 205 """
208 206 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
209 207 return False
210 208 exe = os.path.realpath(sys.executable).lower()
211 209 return (exe.startswith('/usr/bin/python') or
212 210 exe.startswith('/system/library/frameworks/python.framework/'))
213 211
214 212 def _defaultcacerts():
215 213 """return path to CA certificates; None for system's store; ! to disable"""
216 214 if _plainapplepython():
217 215 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
218 216 if os.path.exists(dummycert):
219 217 return dummycert
220 218 if _canloaddefaultcerts:
221 219 return None
222 220 return '!'
223 221
224 222 def sslkwargs(ui, host):
225 223 kws = {'ui': ui}
226 224 hostfingerprint = ui.config('hostfingerprints', host)
227 225 if hostfingerprint:
228 226 return kws
229 227 cacerts = ui.config('web', 'cacerts')
230 228 if cacerts == '!':
231 229 pass
232 230 elif cacerts:
233 231 cacerts = util.expandpath(cacerts)
234 232 if not os.path.exists(cacerts):
235 233 raise error.Abort(_('could not find web.cacerts: %s') % cacerts)
236 234 else:
237 235 cacerts = _defaultcacerts()
238 236 if cacerts and cacerts != '!':
239 237 ui.debug('using %s to enable OS X system CA\n' % cacerts)
240 238 ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts')
241 239 if cacerts != '!':
242 240 kws.update({'ca_certs': cacerts,
243 241 'cert_reqs': ssl.CERT_REQUIRED,
244 242 })
245 243 return kws
246 244
247 245 class validator(object):
248 246 def __init__(self, ui, host):
249 247 self.ui = ui
250 248 self.host = host
251 249
252 250 def __call__(self, sock, strict=False):
253 251 host = self.host
254 252 cacerts = self.ui.config('web', 'cacerts')
255 253 hostfingerprints = self.ui.configlist('hostfingerprints', host)
256 254
257 255 if not sock.cipher(): # work around http://bugs.python.org/issue13721
258 256 raise error.Abort(_('%s ssl connection error') % host)
259 257 try:
260 258 peercert = sock.getpeercert(True)
261 259 peercert2 = sock.getpeercert()
262 260 except AttributeError:
263 261 raise error.Abort(_('%s ssl connection error') % host)
264 262
265 263 if not peercert:
266 264 raise error.Abort(_('%s certificate error: '
267 265 'no certificate received') % host)
268 266 peerfingerprint = util.sha1(peercert).hexdigest()
269 267 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
270 268 for x in xrange(0, len(peerfingerprint), 2)])
271 269 if hostfingerprints:
272 270 fingerprintmatch = False
273 271 for hostfingerprint in hostfingerprints:
274 272 if peerfingerprint.lower() == \
275 273 hostfingerprint.replace(':', '').lower():
276 274 fingerprintmatch = True
277 275 break
278 276 if not fingerprintmatch:
279 277 raise error.Abort(_('certificate for %s has unexpected '
280 278 'fingerprint %s') % (host, nicefingerprint),
281 279 hint=_('check hostfingerprint configuration'))
282 280 self.ui.debug('%s certificate matched fingerprint %s\n' %
283 281 (host, nicefingerprint))
284 282 elif cacerts != '!':
285 283 msg = _verifycert(peercert2, host)
286 284 if msg:
287 285 raise error.Abort(_('%s certificate error: %s') % (host, msg),
288 286 hint=_('configure hostfingerprint %s or use '
289 287 '--insecure to connect insecurely') %
290 288 nicefingerprint)
291 289 self.ui.debug('%s certificate successfully verified\n' % host)
292 290 elif strict:
293 291 raise error.Abort(_('%s certificate with fingerprint %s not '
294 292 'verified') % (host, nicefingerprint),
295 293 hint=_('check hostfingerprints or web.cacerts '
296 294 'config setting'))
297 295 else:
298 296 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
299 297 'verified (check hostfingerprints or web.cacerts '
300 298 'config setting)\n') %
301 299 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now