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