##// END OF EJS Templates
sslutil: use saner TLS settings on Python 2.7.9...
Augie Fackler -
r23850:e1931f7c default
parent child Browse files
Show More
@@ -1,207 +1,217
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 import os, sys
9 import os, sys
10
10
11 from mercurial import util
11 from mercurial import util
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 try:
13 try:
14 # avoid using deprecated/broken FakeSocket in python 2.6
14 # avoid using deprecated/broken FakeSocket in python 2.6
15 import ssl
15 import ssl
16 CERT_REQUIRED = ssl.CERT_REQUIRED
16 CERT_REQUIRED = ssl.CERT_REQUIRED
17 PROTOCOL_TLSv1 = ssl.PROTOCOL_TLSv1
17 PROTOCOL_TLSv1 = ssl.PROTOCOL_TLSv1
18 try:
18 try:
19 ssl_context = ssl.SSLContext
19 ssl_context = ssl.SSLContext
20
20
21 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=ssl.CERT_NONE,
21 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=ssl.CERT_NONE,
22 ca_certs=None, serverhostname=None):
22 ca_certs=None, serverhostname=None):
23 sslcontext = ssl.SSLContext(PROTOCOL_TLSv1)
23 # Allow any version of SSL starting with TLSv1 and
24 # up. Note that specifying TLSv1 here prohibits use of
25 # newer standards (like TLSv1_2), so this is the right way
26 # to do this. Note that in the future it'd be better to
27 # support using ssl.create_default_context(), which sets
28 # up a bunch of things in smart ways (strong ciphers,
29 # protocol versions, etc) and is upgraded by Python
30 # maintainers for us, but that breaks too many things to
31 # do it in a hurry.
32 sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
33 sslcontext.options &= ssl.OP_NO_SSLv2 & ssl.OP_NO_SSLv3
24 if certfile is not None:
34 if certfile is not None:
25 sslcontext.load_cert_chain(certfile, keyfile)
35 sslcontext.load_cert_chain(certfile, keyfile)
26 sslcontext.verify_mode = cert_reqs
36 sslcontext.verify_mode = cert_reqs
27 if ca_certs is not None:
37 if ca_certs is not None:
28 sslcontext.load_verify_locations(cafile=ca_certs)
38 sslcontext.load_verify_locations(cafile=ca_certs)
29
39
30 sslsocket = sslcontext.wrap_socket(sock,
40 sslsocket = sslcontext.wrap_socket(sock,
31 server_hostname=serverhostname)
41 server_hostname=serverhostname)
32 # check if wrap_socket failed silently because socket had been
42 # check if wrap_socket failed silently because socket had been
33 # closed
43 # closed
34 # - see http://bugs.python.org/issue13721
44 # - see http://bugs.python.org/issue13721
35 if not sslsocket.cipher():
45 if not sslsocket.cipher():
36 raise util.Abort(_('ssl connection failed'))
46 raise util.Abort(_('ssl connection failed'))
37 return sslsocket
47 return sslsocket
38 except AttributeError:
48 except AttributeError:
39 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=ssl.CERT_NONE,
49 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=ssl.CERT_NONE,
40 ca_certs=None, serverhostname=None):
50 ca_certs=None, serverhostname=None):
41 sslsocket = ssl.wrap_socket(sock, keyfile, certfile,
51 sslsocket = ssl.wrap_socket(sock, keyfile, certfile,
42 cert_reqs=cert_reqs, ca_certs=ca_certs,
52 cert_reqs=cert_reqs, ca_certs=ca_certs,
43 ssl_version=PROTOCOL_TLSv1)
53 ssl_version=PROTOCOL_TLSv1)
44 # check if wrap_socket failed silently because socket had been
54 # check if wrap_socket failed silently because socket had been
45 # closed
55 # closed
46 # - see http://bugs.python.org/issue13721
56 # - see http://bugs.python.org/issue13721
47 if not sslsocket.cipher():
57 if not sslsocket.cipher():
48 raise util.Abort(_('ssl connection failed'))
58 raise util.Abort(_('ssl connection failed'))
49 return sslsocket
59 return sslsocket
50 except ImportError:
60 except ImportError:
51 CERT_REQUIRED = 2
61 CERT_REQUIRED = 2
52
62
53 PROTOCOL_TLSv1 = 3
63 PROTOCOL_TLSv1 = 3
54
64
55 import socket, httplib
65 import socket, httplib
56
66
57 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=CERT_REQUIRED,
67 def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=CERT_REQUIRED,
58 ca_certs=None, serverhostname=None):
68 ca_certs=None, serverhostname=None):
59 if not util.safehasattr(socket, 'ssl'):
69 if not util.safehasattr(socket, 'ssl'):
60 raise util.Abort(_('Python SSL support not found'))
70 raise util.Abort(_('Python SSL support not found'))
61 if ca_certs:
71 if ca_certs:
62 raise util.Abort(_(
72 raise util.Abort(_(
63 'certificate checking requires Python 2.6'))
73 'certificate checking requires Python 2.6'))
64
74
65 ssl = socket.ssl(sock, keyfile, certfile)
75 ssl = socket.ssl(sock, keyfile, certfile)
66 return httplib.FakeSocket(sock, ssl)
76 return httplib.FakeSocket(sock, ssl)
67
77
68 def _verifycert(cert, hostname):
78 def _verifycert(cert, hostname):
69 '''Verify that cert (in socket.getpeercert() format) matches hostname.
79 '''Verify that cert (in socket.getpeercert() format) matches hostname.
70 CRLs is not handled.
80 CRLs is not handled.
71
81
72 Returns error message if any problems are found and None on success.
82 Returns error message if any problems are found and None on success.
73 '''
83 '''
74 if not cert:
84 if not cert:
75 return _('no certificate received')
85 return _('no certificate received')
76 dnsname = hostname.lower()
86 dnsname = hostname.lower()
77 def matchdnsname(certname):
87 def matchdnsname(certname):
78 return (certname == dnsname or
88 return (certname == dnsname or
79 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
89 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
80
90
81 san = cert.get('subjectAltName', [])
91 san = cert.get('subjectAltName', [])
82 if san:
92 if san:
83 certnames = [value.lower() for key, value in san if key == 'DNS']
93 certnames = [value.lower() for key, value in san if key == 'DNS']
84 for name in certnames:
94 for name in certnames:
85 if matchdnsname(name):
95 if matchdnsname(name):
86 return None
96 return None
87 if certnames:
97 if certnames:
88 return _('certificate is for %s') % ', '.join(certnames)
98 return _('certificate is for %s') % ', '.join(certnames)
89
99
90 # subject is only checked when subjectAltName is empty
100 # subject is only checked when subjectAltName is empty
91 for s in cert.get('subject', []):
101 for s in cert.get('subject', []):
92 key, value = s[0]
102 key, value = s[0]
93 if key == 'commonName':
103 if key == 'commonName':
94 try:
104 try:
95 # 'subject' entries are unicode
105 # 'subject' entries are unicode
96 certname = value.lower().encode('ascii')
106 certname = value.lower().encode('ascii')
97 except UnicodeEncodeError:
107 except UnicodeEncodeError:
98 return _('IDN in certificate not supported')
108 return _('IDN in certificate not supported')
99 if matchdnsname(certname):
109 if matchdnsname(certname):
100 return None
110 return None
101 return _('certificate is for %s') % certname
111 return _('certificate is for %s') % certname
102 return _('no commonName or subjectAltName found in certificate')
112 return _('no commonName or subjectAltName found in certificate')
103
113
104
114
105 # CERT_REQUIRED means fetch the cert from the server all the time AND
115 # CERT_REQUIRED means fetch the cert from the server all the time AND
106 # validate it against the CA store provided in web.cacerts.
116 # validate it against the CA store provided in web.cacerts.
107 #
117 #
108 # We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally
118 # We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally
109 # busted on those versions.
119 # busted on those versions.
110
120
111 def _plainapplepython():
121 def _plainapplepython():
112 """return true if this seems to be a pure Apple Python that
122 """return true if this seems to be a pure Apple Python that
113 * is unfrozen and presumably has the whole mercurial module in the file
123 * is unfrozen and presumably has the whole mercurial module in the file
114 system
124 system
115 * presumably is an Apple Python that uses Apple OpenSSL which has patches
125 * presumably is an Apple Python that uses Apple OpenSSL which has patches
116 for using system certificate store CAs in addition to the provided
126 for using system certificate store CAs in addition to the provided
117 cacerts file
127 cacerts file
118 """
128 """
119 if sys.platform != 'darwin' or util.mainfrozen():
129 if sys.platform != 'darwin' or util.mainfrozen():
120 return False
130 return False
121 exe = (sys.executable or '').lower()
131 exe = (sys.executable or '').lower()
122 return (exe.startswith('/usr/bin/python') or
132 return (exe.startswith('/usr/bin/python') or
123 exe.startswith('/system/library/frameworks/python.framework/'))
133 exe.startswith('/system/library/frameworks/python.framework/'))
124
134
125 def sslkwargs(ui, host):
135 def sslkwargs(ui, host):
126 kws = {}
136 kws = {}
127 hostfingerprint = ui.config('hostfingerprints', host)
137 hostfingerprint = ui.config('hostfingerprints', host)
128 if hostfingerprint:
138 if hostfingerprint:
129 return kws
139 return kws
130 cacerts = ui.config('web', 'cacerts')
140 cacerts = ui.config('web', 'cacerts')
131 if cacerts:
141 if cacerts:
132 cacerts = util.expandpath(cacerts)
142 cacerts = util.expandpath(cacerts)
133 if not os.path.exists(cacerts):
143 if not os.path.exists(cacerts):
134 raise util.Abort(_('could not find web.cacerts: %s') % cacerts)
144 raise util.Abort(_('could not find web.cacerts: %s') % cacerts)
135 elif cacerts is None and _plainapplepython():
145 elif cacerts is None and _plainapplepython():
136 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
146 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
137 if os.path.exists(dummycert):
147 if os.path.exists(dummycert):
138 ui.debug('using %s to enable OS X system CA\n' % dummycert)
148 ui.debug('using %s to enable OS X system CA\n' % dummycert)
139 ui.setconfig('web', 'cacerts', dummycert, 'dummy')
149 ui.setconfig('web', 'cacerts', dummycert, 'dummy')
140 cacerts = dummycert
150 cacerts = dummycert
141 if cacerts:
151 if cacerts:
142 kws.update({'ca_certs': cacerts,
152 kws.update({'ca_certs': cacerts,
143 'cert_reqs': CERT_REQUIRED,
153 'cert_reqs': CERT_REQUIRED,
144 })
154 })
145 return kws
155 return kws
146
156
147 class validator(object):
157 class validator(object):
148 def __init__(self, ui, host):
158 def __init__(self, ui, host):
149 self.ui = ui
159 self.ui = ui
150 self.host = host
160 self.host = host
151
161
152 def __call__(self, sock, strict=False):
162 def __call__(self, sock, strict=False):
153 host = self.host
163 host = self.host
154 cacerts = self.ui.config('web', 'cacerts')
164 cacerts = self.ui.config('web', 'cacerts')
155 hostfingerprint = self.ui.config('hostfingerprints', host)
165 hostfingerprint = self.ui.config('hostfingerprints', host)
156 if not getattr(sock, 'getpeercert', False): # python 2.5 ?
166 if not getattr(sock, 'getpeercert', False): # python 2.5 ?
157 if hostfingerprint:
167 if hostfingerprint:
158 raise util.Abort(_("host fingerprint for %s can't be "
168 raise util.Abort(_("host fingerprint for %s can't be "
159 "verified (Python too old)") % host)
169 "verified (Python too old)") % host)
160 if strict:
170 if strict:
161 raise util.Abort(_("certificate for %s can't be verified "
171 raise util.Abort(_("certificate for %s can't be verified "
162 "(Python too old)") % host)
172 "(Python too old)") % host)
163 if self.ui.configbool('ui', 'reportoldssl', True):
173 if self.ui.configbool('ui', 'reportoldssl', True):
164 self.ui.warn(_("warning: certificate for %s can't be verified "
174 self.ui.warn(_("warning: certificate for %s can't be verified "
165 "(Python too old)\n") % host)
175 "(Python too old)\n") % host)
166 return
176 return
167
177
168 if not sock.cipher(): # work around http://bugs.python.org/issue13721
178 if not sock.cipher(): # work around http://bugs.python.org/issue13721
169 raise util.Abort(_('%s ssl connection error') % host)
179 raise util.Abort(_('%s ssl connection error') % host)
170 try:
180 try:
171 peercert = sock.getpeercert(True)
181 peercert = sock.getpeercert(True)
172 peercert2 = sock.getpeercert()
182 peercert2 = sock.getpeercert()
173 except AttributeError:
183 except AttributeError:
174 raise util.Abort(_('%s ssl connection error') % host)
184 raise util.Abort(_('%s ssl connection error') % host)
175
185
176 if not peercert:
186 if not peercert:
177 raise util.Abort(_('%s certificate error: '
187 raise util.Abort(_('%s certificate error: '
178 'no certificate received') % host)
188 'no certificate received') % host)
179 peerfingerprint = util.sha1(peercert).hexdigest()
189 peerfingerprint = util.sha1(peercert).hexdigest()
180 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
190 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
181 for x in xrange(0, len(peerfingerprint), 2)])
191 for x in xrange(0, len(peerfingerprint), 2)])
182 if hostfingerprint:
192 if hostfingerprint:
183 if peerfingerprint.lower() != \
193 if peerfingerprint.lower() != \
184 hostfingerprint.replace(':', '').lower():
194 hostfingerprint.replace(':', '').lower():
185 raise util.Abort(_('certificate for %s has unexpected '
195 raise util.Abort(_('certificate for %s has unexpected '
186 'fingerprint %s') % (host, nicefingerprint),
196 'fingerprint %s') % (host, nicefingerprint),
187 hint=_('check hostfingerprint configuration'))
197 hint=_('check hostfingerprint configuration'))
188 self.ui.debug('%s certificate matched fingerprint %s\n' %
198 self.ui.debug('%s certificate matched fingerprint %s\n' %
189 (host, nicefingerprint))
199 (host, nicefingerprint))
190 elif cacerts:
200 elif cacerts:
191 msg = _verifycert(peercert2, host)
201 msg = _verifycert(peercert2, host)
192 if msg:
202 if msg:
193 raise util.Abort(_('%s certificate error: %s') % (host, msg),
203 raise util.Abort(_('%s certificate error: %s') % (host, msg),
194 hint=_('configure hostfingerprint %s or use '
204 hint=_('configure hostfingerprint %s or use '
195 '--insecure to connect insecurely') %
205 '--insecure to connect insecurely') %
196 nicefingerprint)
206 nicefingerprint)
197 self.ui.debug('%s certificate successfully verified\n' % host)
207 self.ui.debug('%s certificate successfully verified\n' % host)
198 elif strict:
208 elif strict:
199 raise util.Abort(_('%s certificate with fingerprint %s not '
209 raise util.Abort(_('%s certificate with fingerprint %s not '
200 'verified') % (host, nicefingerprint),
210 'verified') % (host, nicefingerprint),
201 hint=_('check hostfingerprints or web.cacerts '
211 hint=_('check hostfingerprints or web.cacerts '
202 'config setting'))
212 'config setting'))
203 else:
213 else:
204 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
214 self.ui.warn(_('warning: %s certificate with fingerprint %s not '
205 'verified (check hostfingerprints or web.cacerts '
215 'verified (check hostfingerprints or web.cacerts '
206 'config setting)\n') %
216 'config setting)\n') %
207 (host, nicefingerprint))
217 (host, nicefingerprint))
General Comments 0
You need to be logged in to leave comments. Login now