Show More
@@ -0,0 +1,126 b'' | |||||
|
1 | # sslutil.py - SSL handling for mercurial | |||
|
2 | # | |||
|
3 | # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com> | |||
|
4 | # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br> | |||
|
5 | # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | |||
|
6 | # | |||
|
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. | |||
|
9 | import os | |||
|
10 | ||||
|
11 | from mercurial import util | |||
|
12 | from mercurial.i18n import _ | |||
|
13 | try: | |||
|
14 | # avoid using deprecated/broken FakeSocket in python 2.6 | |||
|
15 | import ssl | |||
|
16 | ssl_wrap_socket = ssl.wrap_socket | |||
|
17 | CERT_REQUIRED = ssl.CERT_REQUIRED | |||
|
18 | except ImportError: | |||
|
19 | CERT_REQUIRED = 2 | |||
|
20 | ||||
|
21 | def ssl_wrap_socket(sock, key_file, cert_file, | |||
|
22 | cert_reqs=CERT_REQUIRED, ca_certs=None): | |||
|
23 | if ca_certs: | |||
|
24 | raise util.Abort(_( | |||
|
25 | 'certificate checking requires Python 2.6')) | |||
|
26 | ||||
|
27 | ssl = socket.ssl(sock, key_file, cert_file) | |||
|
28 | return httplib.FakeSocket(sock, ssl) | |||
|
29 | ||||
|
30 | def _verifycert(cert, hostname): | |||
|
31 | '''Verify that cert (in socket.getpeercert() format) matches hostname. | |||
|
32 | CRLs is not handled. | |||
|
33 | ||||
|
34 | Returns error message if any problems are found and None on success. | |||
|
35 | ''' | |||
|
36 | if not cert: | |||
|
37 | return _('no certificate received') | |||
|
38 | dnsname = hostname.lower() | |||
|
39 | def matchdnsname(certname): | |||
|
40 | return (certname == dnsname or | |||
|
41 | '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]) | |||
|
42 | ||||
|
43 | san = cert.get('subjectAltName', []) | |||
|
44 | if san: | |||
|
45 | certnames = [value.lower() for key, value in san if key == 'DNS'] | |||
|
46 | for name in certnames: | |||
|
47 | if matchdnsname(name): | |||
|
48 | return None | |||
|
49 | return _('certificate is for %s') % ', '.join(certnames) | |||
|
50 | ||||
|
51 | # subject is only checked when subjectAltName is empty | |||
|
52 | for s in cert.get('subject', []): | |||
|
53 | key, value = s[0] | |||
|
54 | if key == 'commonName': | |||
|
55 | try: | |||
|
56 | # 'subject' entries are unicode | |||
|
57 | certname = value.lower().encode('ascii') | |||
|
58 | except UnicodeEncodeError: | |||
|
59 | return _('IDN in certificate not supported') | |||
|
60 | if matchdnsname(certname): | |||
|
61 | return None | |||
|
62 | return _('certificate is for %s') % certname | |||
|
63 | return _('no commonName or subjectAltName found in certificate') | |||
|
64 | ||||
|
65 | ||||
|
66 | # CERT_REQUIRED means fetch the cert from the server all the time AND | |||
|
67 | # validate it against the CA store provided in web.cacerts. | |||
|
68 | # | |||
|
69 | # We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally | |||
|
70 | # busted on those versions. | |||
|
71 | ||||
|
72 | def sslkwargs(ui, host): | |||
|
73 | cacerts = ui.config('web', 'cacerts') | |||
|
74 | hostfingerprint = ui.config('hostfingerprints', host) | |||
|
75 | if cacerts and not hostfingerprint: | |||
|
76 | cacerts = util.expandpath(cacerts) | |||
|
77 | if not os.path.exists(cacerts): | |||
|
78 | raise util.Abort(_('could not find web.cacerts: %s') % cacerts) | |||
|
79 | return {'ca_certs': cacerts, | |||
|
80 | 'cert_reqs': CERT_REQUIRED, | |||
|
81 | } | |||
|
82 | return {} | |||
|
83 | ||||
|
84 | class validator(object): | |||
|
85 | def __init__(self, ui, host): | |||
|
86 | self.ui = ui | |||
|
87 | self.host = host | |||
|
88 | ||||
|
89 | def __call__(self, sock): | |||
|
90 | host = self.host | |||
|
91 | cacerts = self.ui.config('web', 'cacerts') | |||
|
92 | hostfingerprint = self.ui.config('hostfingerprints', host) | |||
|
93 | if cacerts and not hostfingerprint: | |||
|
94 | msg = _verifycert(sock.getpeercert(), host) | |||
|
95 | if msg: | |||
|
96 | raise util.Abort(_('%s certificate error: %s ' | |||
|
97 | '(use --insecure to connect ' | |||
|
98 | 'insecurely)') % (host, msg)) | |||
|
99 | self.ui.debug('%s certificate successfully verified\n' % host) | |||
|
100 | else: | |||
|
101 | if getattr(sock, 'getpeercert', False): | |||
|
102 | peercert = sock.getpeercert(True) | |||
|
103 | peerfingerprint = util.sha1(peercert).hexdigest() | |||
|
104 | nicefingerprint = ":".join([peerfingerprint[x:x + 2] | |||
|
105 | for x in xrange(0, len(peerfingerprint), 2)]) | |||
|
106 | if hostfingerprint: | |||
|
107 | if peerfingerprint.lower() != \ | |||
|
108 | hostfingerprint.replace(':', '').lower(): | |||
|
109 | raise util.Abort(_('invalid certificate for %s ' | |||
|
110 | 'with fingerprint %s') % | |||
|
111 | (host, nicefingerprint)) | |||
|
112 | self.ui.debug('%s certificate matched fingerprint %s\n' % | |||
|
113 | (host, nicefingerprint)) | |||
|
114 | else: | |||
|
115 | self.ui.warn(_('warning: %s certificate ' | |||
|
116 | 'with fingerprint %s not verified ' | |||
|
117 | '(check hostfingerprints or web.cacerts ' | |||
|
118 | 'config setting)\n') % | |||
|
119 | (host, nicefingerprint)) | |||
|
120 | else: # python 2.5 ? | |||
|
121 | if hostfingerprint: | |||
|
122 | raise util.Abort(_('no certificate for %s with ' | |||
|
123 | 'configured hostfingerprint') % host) | |||
|
124 | self.ui.warn(_('warning: %s certificate not verified ' | |||
|
125 | '(check web.cacerts config setting)\n') % | |||
|
126 | host) |
@@ -10,7 +10,7 b'' | |||||
10 | import urllib, urllib2, httplib, os, socket, cStringIO |
|
10 | import urllib, urllib2, httplib, os, socket, cStringIO | |
11 | import __builtin__ |
|
11 | import __builtin__ | |
12 | from i18n import _ |
|
12 | from i18n import _ | |
13 | import keepalive, util |
|
13 | import keepalive, util, sslutil | |
14 |
|
14 | |||
15 | def readauthforuri(ui, uri): |
|
15 | def readauthforuri(ui, uri): | |
16 | # Read configuration |
|
16 | # Read configuration | |
@@ -202,23 +202,6 b' def _gen_sendfile(orgsend):' | |||||
202 | has_https = hasattr(urllib2, 'HTTPSHandler') |
|
202 | has_https = hasattr(urllib2, 'HTTPSHandler') | |
203 | if has_https: |
|
203 | if has_https: | |
204 | try: |
|
204 | try: | |
205 | # avoid using deprecated/broken FakeSocket in python 2.6 |
|
|||
206 | import ssl |
|
|||
207 | _ssl_wrap_socket = ssl.wrap_socket |
|
|||
208 | CERT_REQUIRED = ssl.CERT_REQUIRED |
|
|||
209 | except ImportError: |
|
|||
210 | CERT_REQUIRED = 2 |
|
|||
211 |
|
||||
212 | def _ssl_wrap_socket(sock, key_file, cert_file, |
|
|||
213 | cert_reqs=CERT_REQUIRED, ca_certs=None): |
|
|||
214 | if ca_certs: |
|
|||
215 | raise util.Abort(_( |
|
|||
216 | 'certificate checking requires Python 2.6')) |
|
|||
217 |
|
||||
218 | ssl = socket.ssl(sock, key_file, cert_file) |
|
|||
219 | return httplib.FakeSocket(sock, ssl) |
|
|||
220 |
|
||||
221 | try: |
|
|||
222 | _create_connection = socket.create_connection |
|
205 | _create_connection = socket.create_connection | |
223 | except AttributeError: |
|
206 | except AttributeError: | |
224 | _GLOBAL_DEFAULT_TIMEOUT = object() |
|
207 | _GLOBAL_DEFAULT_TIMEOUT = object() | |
@@ -257,7 +240,7 b' class httpconnection(keepalive.HTTPConne' | |||||
257 | self.sock.connect((self.host, self.port)) |
|
240 | self.sock.connect((self.host, self.port)) | |
258 | if _generic_proxytunnel(self): |
|
241 | if _generic_proxytunnel(self): | |
259 | # we do not support client x509 certificates |
|
242 | # we do not support client x509 certificates | |
260 |
self.sock = |
|
243 | self.sock = sslutil.ssl_wrap_socket(self.sock, None, None) | |
261 | else: |
|
244 | else: | |
262 | keepalive.HTTPConnection.connect(self) |
|
245 | keepalive.HTTPConnection.connect(self) | |
263 |
|
246 | |||
@@ -398,41 +381,6 b' class httphandler(keepalive.HTTPHandler)' | |||||
398 | _generic_start_transaction(self, h, req) |
|
381 | _generic_start_transaction(self, h, req) | |
399 | return keepalive.HTTPHandler._start_transaction(self, h, req) |
|
382 | return keepalive.HTTPHandler._start_transaction(self, h, req) | |
400 |
|
383 | |||
401 | def _verifycert(cert, hostname): |
|
|||
402 | '''Verify that cert (in socket.getpeercert() format) matches hostname. |
|
|||
403 | CRLs is not handled. |
|
|||
404 |
|
||||
405 | Returns error message if any problems are found and None on success. |
|
|||
406 | ''' |
|
|||
407 | if not cert: |
|
|||
408 | return _('no certificate received') |
|
|||
409 | dnsname = hostname.lower() |
|
|||
410 | def matchdnsname(certname): |
|
|||
411 | return (certname == dnsname or |
|
|||
412 | '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]) |
|
|||
413 |
|
||||
414 | san = cert.get('subjectAltName', []) |
|
|||
415 | if san: |
|
|||
416 | certnames = [value.lower() for key, value in san if key == 'DNS'] |
|
|||
417 | for name in certnames: |
|
|||
418 | if matchdnsname(name): |
|
|||
419 | return None |
|
|||
420 | return _('certificate is for %s') % ', '.join(certnames) |
|
|||
421 |
|
||||
422 | # subject is only checked when subjectAltName is empty |
|
|||
423 | for s in cert.get('subject', []): |
|
|||
424 | key, value = s[0] |
|
|||
425 | if key == 'commonName': |
|
|||
426 | try: |
|
|||
427 | # 'subject' entries are unicode |
|
|||
428 | certname = value.lower().encode('ascii') |
|
|||
429 | except UnicodeEncodeError: |
|
|||
430 | return _('IDN in certificate not supported') |
|
|||
431 | if matchdnsname(certname): |
|
|||
432 | return None |
|
|||
433 | return _('certificate is for %s') % certname |
|
|||
434 | return _('no commonName or subjectAltName found in certificate') |
|
|||
435 |
|
||||
436 | if has_https: |
|
384 | if has_https: | |
437 | class httpsconnection(httplib.HTTPSConnection): |
|
385 | class httpsconnection(httplib.HTTPSConnection): | |
438 | response_class = keepalive.HTTPResponse |
|
386 | response_class = keepalive.HTTPResponse | |
@@ -447,53 +395,10 b' if has_https:' | |||||
447 | if self.realhostport: # use CONNECT proxy |
|
395 | if self.realhostport: # use CONNECT proxy | |
448 | _generic_proxytunnel(self) |
|
396 | _generic_proxytunnel(self) | |
449 | host = self.realhostport.rsplit(':', 1)[0] |
|
397 | host = self.realhostport.rsplit(':', 1)[0] | |
450 |
|
398 | self.sock = sslutil.ssl_wrap_socket( | ||
451 | cacerts = self.ui.config('web', 'cacerts') |
|
399 | self.sock, self.key_file, self.cert_file, | |
452 | hostfingerprint = self.ui.config('hostfingerprints', host) |
|
400 | **sslutil.sslkwargs(self.ui, host)) | |
453 |
|
401 | sslutil.validator(self.ui, host)(self.sock) | ||
454 | if cacerts and not hostfingerprint: |
|
|||
455 | cacerts = util.expandpath(cacerts) |
|
|||
456 | if not os.path.exists(cacerts): |
|
|||
457 | raise util.Abort(_('could not find ' |
|
|||
458 | 'web.cacerts: %s') % cacerts) |
|
|||
459 | self.sock = _ssl_wrap_socket(self.sock, self.key_file, |
|
|||
460 | self.cert_file, cert_reqs=CERT_REQUIRED, |
|
|||
461 | ca_certs=cacerts) |
|
|||
462 | msg = _verifycert(self.sock.getpeercert(), host) |
|
|||
463 | if msg: |
|
|||
464 | raise util.Abort(_('%s certificate error: %s ' |
|
|||
465 | '(use --insecure to connect ' |
|
|||
466 | 'insecurely)') % (host, msg)) |
|
|||
467 | self.ui.debug('%s certificate successfully verified\n' % host) |
|
|||
468 | else: |
|
|||
469 | self.sock = _ssl_wrap_socket(self.sock, self.key_file, |
|
|||
470 | self.cert_file) |
|
|||
471 | if hasattr(self.sock, 'getpeercert'): |
|
|||
472 | peercert = self.sock.getpeercert(True) |
|
|||
473 | peerfingerprint = util.sha1(peercert).hexdigest() |
|
|||
474 | nicefingerprint = ":".join([peerfingerprint[x:x + 2] |
|
|||
475 | for x in xrange(0, len(peerfingerprint), 2)]) |
|
|||
476 | if hostfingerprint: |
|
|||
477 | if peerfingerprint.lower() != \ |
|
|||
478 | hostfingerprint.replace(':', '').lower(): |
|
|||
479 | raise util.Abort(_('invalid certificate for %s ' |
|
|||
480 | 'with fingerprint %s') % |
|
|||
481 | (host, nicefingerprint)) |
|
|||
482 | self.ui.debug('%s certificate matched fingerprint %s\n' % |
|
|||
483 | (host, nicefingerprint)) |
|
|||
484 | else: |
|
|||
485 | self.ui.warn(_('warning: %s certificate ' |
|
|||
486 | 'with fingerprint %s not verified ' |
|
|||
487 | '(check hostfingerprints or web.cacerts ' |
|
|||
488 | 'config setting)\n') % |
|
|||
489 | (host, nicefingerprint)) |
|
|||
490 | else: # python 2.5 ? |
|
|||
491 | if hostfingerprint: |
|
|||
492 | raise util.Abort(_('no certificate for %s with ' |
|
|||
493 | 'configured hostfingerprint') % host) |
|
|||
494 | self.ui.warn(_('warning: %s certificate not verified ' |
|
|||
495 | '(check web.cacerts config setting)\n') % |
|
|||
496 | host) |
|
|||
497 |
|
402 | |||
498 | class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): |
|
403 | class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): | |
499 | def __init__(self, ui): |
|
404 | def __init__(self, ui): |
@@ -7,7 +7,7 b' def check(a, b):' | |||||
7 | def cert(cn): |
|
7 | def cert(cn): | |
8 | return dict(subject=((('commonName', cn),),)) |
|
8 | return dict(subject=((('commonName', cn),),)) | |
9 |
|
9 | |||
10 |
from mercurial. |
|
10 | from mercurial.sslutil import _verifycert | |
11 |
|
11 | |||
12 | # Test non-wildcard certificates |
|
12 | # Test non-wildcard certificates | |
13 | check(_verifycert(cert('example.com'), 'example.com'), |
|
13 | check(_verifycert(cert('example.com'), 'example.com'), |
General Comments 0
You need to be logged in to leave comments.
Login now