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