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