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