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