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