##// END OF EJS Templates
sslutil: fall back to commonName when no dNSName in subjectAltName (issue2798)...
Nicolas Bareil -
r14666:27b080aa default
parent child Browse files
Show More
@@ -1,128 +1,129
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 import os
10 10
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13 try:
14 14 # avoid using deprecated/broken FakeSocket in python 2.6
15 15 import ssl
16 16 ssl_wrap_socket = ssl.wrap_socket
17 17 CERT_REQUIRED = ssl.CERT_REQUIRED
18 18 except ImportError:
19 19 CERT_REQUIRED = 2
20 20
21 21 import socket, httplib
22 22
23 23 def ssl_wrap_socket(sock, key_file, cert_file,
24 24 cert_reqs=CERT_REQUIRED, ca_certs=None):
25 25 if ca_certs:
26 26 raise util.Abort(_(
27 27 'certificate checking requires Python 2.6'))
28 28
29 29 ssl = socket.ssl(sock, key_file, cert_file)
30 30 return httplib.FakeSocket(sock, ssl)
31 31
32 32 def _verifycert(cert, hostname):
33 33 '''Verify that cert (in socket.getpeercert() format) matches hostname.
34 34 CRLs is not handled.
35 35
36 36 Returns error message if any problems are found and None on success.
37 37 '''
38 38 if not cert:
39 39 return _('no certificate received')
40 40 dnsname = hostname.lower()
41 41 def matchdnsname(certname):
42 42 return (certname == dnsname or
43 43 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
44 44
45 45 san = cert.get('subjectAltName', [])
46 46 if san:
47 47 certnames = [value.lower() for key, value in san if key == 'DNS']
48 48 for name in certnames:
49 49 if matchdnsname(name):
50 50 return None
51 return _('certificate is for %s') % ', '.join(certnames)
51 if certnames:
52 return _('certificate is for %s') % ', '.join(certnames)
52 53
53 54 # subject is only checked when subjectAltName is empty
54 55 for s in cert.get('subject', []):
55 56 key, value = s[0]
56 57 if key == 'commonName':
57 58 try:
58 59 # 'subject' entries are unicode
59 60 certname = value.lower().encode('ascii')
60 61 except UnicodeEncodeError:
61 62 return _('IDN in certificate not supported')
62 63 if matchdnsname(certname):
63 64 return None
64 65 return _('certificate is for %s') % certname
65 66 return _('no commonName or subjectAltName found in certificate')
66 67
67 68
68 69 # CERT_REQUIRED means fetch the cert from the server all the time AND
69 70 # validate it against the CA store provided in web.cacerts.
70 71 #
71 72 # We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally
72 73 # busted on those versions.
73 74
74 75 def sslkwargs(ui, host):
75 76 cacerts = ui.config('web', 'cacerts')
76 77 hostfingerprint = ui.config('hostfingerprints', host)
77 78 if cacerts and not hostfingerprint:
78 79 cacerts = util.expandpath(cacerts)
79 80 if not os.path.exists(cacerts):
80 81 raise util.Abort(_('could not find web.cacerts: %s') % cacerts)
81 82 return {'ca_certs': cacerts,
82 83 'cert_reqs': CERT_REQUIRED,
83 84 }
84 85 return {}
85 86
86 87 class validator(object):
87 88 def __init__(self, ui, host):
88 89 self.ui = ui
89 90 self.host = host
90 91
91 92 def __call__(self, sock):
92 93 host = self.host
93 94 cacerts = self.ui.config('web', 'cacerts')
94 95 hostfingerprint = self.ui.config('hostfingerprints', host)
95 96 if cacerts and not hostfingerprint:
96 97 msg = _verifycert(sock.getpeercert(), host)
97 98 if msg:
98 99 raise util.Abort(_('%s certificate error: %s '
99 100 '(use --insecure to connect '
100 101 'insecurely)') % (host, msg))
101 102 self.ui.debug('%s certificate successfully verified\n' % host)
102 103 else:
103 104 if getattr(sock, 'getpeercert', False):
104 105 peercert = sock.getpeercert(True)
105 106 peerfingerprint = util.sha1(peercert).hexdigest()
106 107 nicefingerprint = ":".join([peerfingerprint[x:x + 2]
107 108 for x in xrange(0, len(peerfingerprint), 2)])
108 109 if hostfingerprint:
109 110 if peerfingerprint.lower() != \
110 111 hostfingerprint.replace(':', '').lower():
111 112 raise util.Abort(_('invalid certificate for %s '
112 113 'with fingerprint %s') %
113 114 (host, nicefingerprint))
114 115 self.ui.debug('%s certificate matched fingerprint %s\n' %
115 116 (host, nicefingerprint))
116 117 else:
117 118 self.ui.warn(_('warning: %s certificate '
118 119 'with fingerprint %s not verified '
119 120 '(check hostfingerprints or web.cacerts '
120 121 'config setting)\n') %
121 122 (host, nicefingerprint))
122 123 else: # python 2.5 ?
123 124 if hostfingerprint:
124 125 raise util.Abort(_('no certificate for %s with '
125 126 'configured hostfingerprint') % host)
126 127 self.ui.warn(_('warning: %s certificate not verified '
127 128 '(check web.cacerts config setting)\n') %
128 129 host)
@@ -1,217 +1,221
1 1 import sys
2 2
3 3 def check(a, b):
4 4 if a != b:
5 5 print (a, b)
6 6
7 7 def cert(cn):
8 8 return dict(subject=((('commonName', cn),),))
9 9
10 10 from mercurial.sslutil import _verifycert
11 11
12 12 # Test non-wildcard certificates
13 13 check(_verifycert(cert('example.com'), 'example.com'),
14 14 None)
15 15 check(_verifycert(cert('example.com'), 'www.example.com'),
16 16 'certificate is for example.com')
17 17 check(_verifycert(cert('www.example.com'), 'example.com'),
18 18 'certificate is for www.example.com')
19 19
20 20 # Test wildcard certificates
21 21 check(_verifycert(cert('*.example.com'), 'www.example.com'),
22 22 None)
23 23 check(_verifycert(cert('*.example.com'), 'example.com'),
24 24 'certificate is for *.example.com')
25 25 check(_verifycert(cert('*.example.com'), 'w.w.example.com'),
26 26 'certificate is for *.example.com')
27 27
28 28 # Test subjectAltName
29 29 san_cert = {'subject': ((('commonName', 'example.com'),),),
30 30 'subjectAltName': (('DNS', '*.example.net'),
31 31 ('DNS', 'example.net'))}
32 32 check(_verifycert(san_cert, 'example.net'),
33 33 None)
34 34 check(_verifycert(san_cert, 'foo.example.net'),
35 35 None)
36 # subject is only checked when subjectAltName is empty
36 # no fallback to subject commonName when subjectAltName has DNS
37 37 check(_verifycert(san_cert, 'example.com'),
38 38 'certificate is for *.example.net, example.net')
39 # fallback to subject commonName when no DNS in subjectAltName
40 san_cert = {'subject': ((('commonName', 'example.com'),),),
41 'subjectAltName': (('IP Address', '8.8.8.8'),)}
42 check(_verifycert(san_cert, 'example.com'), None)
39 43
40 44 # Avoid some pitfalls
41 45 check(_verifycert(cert('*.foo'), 'foo'),
42 46 'certificate is for *.foo')
43 47 check(_verifycert(cert('*o'), 'foo'),
44 48 'certificate is for *o')
45 49
46 50 check(_verifycert({'subject': ()},
47 51 'example.com'),
48 52 'no commonName or subjectAltName found in certificate')
49 53 check(_verifycert(None, 'example.com'),
50 54 'no certificate received')
51 55
56 # Unicode (IDN) certname isn't supported
57 check(_verifycert(cert(u'\u4f8b.jp'), 'example.jp'),
58 'IDN in certificate not supported')
59
52 60 import doctest
53 61
54 62 def test_url():
55 63 """
56 64 >>> from mercurial.util import url
57 65
58 66 This tests for edge cases in url.URL's parsing algorithm. Most of
59 67 these aren't useful for documentation purposes, so they aren't
60 68 part of the class's doc tests.
61 69
62 70 Query strings and fragments:
63 71
64 72 >>> url('http://host/a?b#c')
65 73 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
66 74 >>> url('http://host/a?')
67 75 <url scheme: 'http', host: 'host', path: 'a'>
68 76 >>> url('http://host/a#b#c')
69 77 <url scheme: 'http', host: 'host', path: 'a', fragment: 'b#c'>
70 78 >>> url('http://host/a#b?c')
71 79 <url scheme: 'http', host: 'host', path: 'a', fragment: 'b?c'>
72 80 >>> url('http://host/?a#b')
73 81 <url scheme: 'http', host: 'host', path: '', query: 'a', fragment: 'b'>
74 82 >>> url('http://host/?a#b', parsequery=False)
75 83 <url scheme: 'http', host: 'host', path: '?a', fragment: 'b'>
76 84 >>> url('http://host/?a#b', parsefragment=False)
77 85 <url scheme: 'http', host: 'host', path: '', query: 'a#b'>
78 86 >>> url('http://host/?a#b', parsequery=False, parsefragment=False)
79 87 <url scheme: 'http', host: 'host', path: '?a#b'>
80 88
81 89 IPv6 addresses:
82 90
83 91 >>> url('ldap://[2001:db8::7]/c=GB?objectClass?one')
84 92 <url scheme: 'ldap', host: '[2001:db8::7]', path: 'c=GB',
85 93 query: 'objectClass?one'>
86 94 >>> url('ldap://joe:xxx@[2001:db8::7]:80/c=GB?objectClass?one')
87 95 <url scheme: 'ldap', user: 'joe', passwd: 'xxx', host: '[2001:db8::7]',
88 96 port: '80', path: 'c=GB', query: 'objectClass?one'>
89 97
90 98 Missing scheme, host, etc.:
91 99
92 100 >>> url('://192.0.2.16:80/')
93 101 <url path: '://192.0.2.16:80/'>
94 102 >>> url('http://mercurial.selenic.com')
95 103 <url scheme: 'http', host: 'mercurial.selenic.com'>
96 104 >>> url('/foo')
97 105 <url path: '/foo'>
98 106 >>> url('bundle:/foo')
99 107 <url scheme: 'bundle', path: '/foo'>
100 108 >>> url('a?b#c')
101 109 <url path: 'a?b', fragment: 'c'>
102 110 >>> url('http://x.com?arg=/foo')
103 111 <url scheme: 'http', host: 'x.com', query: 'arg=/foo'>
104 112 >>> url('http://joe:xxx@/foo')
105 113 <url scheme: 'http', user: 'joe', passwd: 'xxx', path: 'foo'>
106 114
107 115 Just a scheme and a path:
108 116
109 117 >>> url('mailto:John.Doe@example.com')
110 118 <url scheme: 'mailto', path: 'John.Doe@example.com'>
111 119 >>> url('a:b:c:d')
112 120 <url path: 'a:b:c:d'>
113 121 >>> url('aa:bb:cc:dd')
114 122 <url scheme: 'aa', path: 'bb:cc:dd'>
115 123
116 124 SSH examples:
117 125
118 126 >>> url('ssh://joe@host//home/joe')
119 127 <url scheme: 'ssh', user: 'joe', host: 'host', path: '/home/joe'>
120 128 >>> url('ssh://joe:xxx@host/src')
121 129 <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host', path: 'src'>
122 130 >>> url('ssh://joe:xxx@host')
123 131 <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host'>
124 132 >>> url('ssh://joe@host')
125 133 <url scheme: 'ssh', user: 'joe', host: 'host'>
126 134 >>> url('ssh://host')
127 135 <url scheme: 'ssh', host: 'host'>
128 136 >>> url('ssh://')
129 137 <url scheme: 'ssh'>
130 138 >>> url('ssh:')
131 139 <url scheme: 'ssh'>
132 140
133 141 Non-numeric port:
134 142
135 143 >>> url('http://example.com:dd')
136 144 <url scheme: 'http', host: 'example.com', port: 'dd'>
137 145 >>> url('ssh://joe:xxx@host:ssh/foo')
138 146 <url scheme: 'ssh', user: 'joe', passwd: 'xxx', host: 'host', port: 'ssh',
139 147 path: 'foo'>
140 148
141 149 Bad authentication credentials:
142 150
143 151 >>> url('http://joe@joeville:123@4:@host/a?b#c')
144 152 <url scheme: 'http', user: 'joe@joeville', passwd: '123@4:',
145 153 host: 'host', path: 'a', query: 'b', fragment: 'c'>
146 154 >>> url('http://!*#?/@!*#?/:@host/a?b#c')
147 155 <url scheme: 'http', host: '!*', fragment: '?/@!*#?/:@host/a?b#c'>
148 156 >>> url('http://!*#?@!*#?:@host/a?b#c')
149 157 <url scheme: 'http', host: '!*', fragment: '?@!*#?:@host/a?b#c'>
150 158 >>> url('http://!*@:!*@@host/a?b#c')
151 159 <url scheme: 'http', user: '!*@', passwd: '!*@', host: 'host',
152 160 path: 'a', query: 'b', fragment: 'c'>
153 161
154 162 File paths:
155 163
156 164 >>> url('a/b/c/d.g.f')
157 165 <url path: 'a/b/c/d.g.f'>
158 166 >>> url('/x///z/y/')
159 167 <url path: '/x///z/y/'>
160 168 >>> url('/foo:bar')
161 169 <url path: '/foo:bar'>
162 170 >>> url('\\\\foo:bar')
163 171 <url path: '\\\\foo:bar'>
164 172 >>> url('./foo:bar')
165 173 <url path: './foo:bar'>
166 174
167 175 Non-localhost file URL:
168 176
169 177 >>> u = url('file://mercurial.selenic.com/foo')
170 178 Traceback (most recent call last):
171 179 File "<stdin>", line 1, in ?
172 180 Abort: file:// URLs can only refer to localhost
173 181
174 182 Empty URL:
175 183
176 184 >>> u = url('')
177 185 >>> u
178 186 <url path: ''>
179 187 >>> str(u)
180 188 ''
181 189
182 190 Empty path with query string:
183 191
184 192 >>> str(url('http://foo/?bar'))
185 193 'http://foo/?bar'
186 194
187 195 Invalid path:
188 196
189 197 >>> u = url('http://foo/bar')
190 198 >>> u.path = 'bar'
191 199 >>> str(u)
192 200 'http://foo/bar'
193 201
194 202 >>> u = url('file:/foo/bar/baz')
195 203 >>> u
196 204 <url scheme: 'file', path: '/foo/bar/baz'>
197 205 >>> str(u)
198 206 'file:///foo/bar/baz'
199 207
200 208 >>> u = url('file:///foo/bar/baz')
201 209 >>> u
202 210 <url scheme: 'file', path: '/foo/bar/baz'>
203 211 >>> str(u)
204 212 'file:///foo/bar/baz'
205 213
206 214 >>> u = url('file:foo/bar/baz')
207 215 >>> u
208 216 <url scheme: 'file', path: 'foo/bar/baz'>
209 217 >>> str(u)
210 218 'file:foo/bar/baz'
211 219 """
212 220
213 221 doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE)
214
215 # Unicode (IDN) certname isn't supported
216 check(_verifycert(cert(u'\u4f8b.jp'), 'example.jp'),
217 'IDN in certificate not supported')
General Comments 0
You need to be logged in to leave comments. Login now