diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -951,8 +951,9 @@ Web interface configuration. third-party tools like email notification hooks can construct URLs. Example: ``http://hgserver/repos/``. ``cacerts`` - Path to file containing a list of PEM encoded certificate authorities - that may be used to verify an SSL server's identity. The form must be + Path to file containing a list of PEM encoded certificate authority + certificates. If specified on the client, then it will verify the identity + of remote HTTPS servers with these certificates. The form must be as follows:: -----BEGIN CERTIFICATE----- @@ -962,8 +963,8 @@ Web interface configuration. ... (certificate in base64 PEM encoding) ... -----END CERTIFICATE----- - This feature is only supported when using Python 2.6. If you wish to - use it with earlier versions of Python, install the backported + This feature is only supported when using Python 2.6 or later. If you wish + to use it with earlier versions of Python, install the backported version of the ssl library that is available from ``http://pypi.python.org``. diff --git a/mercurial/help/urls.txt b/mercurial/help/urls.txt --- a/mercurial/help/urls.txt +++ b/mercurial/help/urls.txt @@ -18,6 +18,9 @@ Some features, such as pushing to http:/ possible if the feature is explicitly enabled on the remote Mercurial server. +Note that the security of HTTPS URLs depends on proper configuration of +web.cacerts. + Some notes about using SSH with Mercurial: - SSH requires an accessible shell account on the destination machine diff --git a/mercurial/url.py b/mercurial/url.py --- a/mercurial/url.py +++ b/mercurial/url.py @@ -7,7 +7,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import urllib, urllib2, urlparse, httplib, os, re, socket, cStringIO +import urllib, urllib2, urlparse, httplib, os, re, socket, cStringIO, time import __builtin__ from i18n import _ import keepalive, util @@ -486,6 +486,31 @@ class httphandler(keepalive.HTTPHandler) _generic_start_transaction(self, h, req) return keepalive.HTTPHandler._start_transaction(self, h, req) +def _verifycert(cert, hostname): + '''Verify that cert (in socket.getpeercert() format) matches hostname and is + valid at this time. CRLs and subjectAltName are not handled. + + Returns error message if any problems are found and None on success. + ''' + if not cert: + return _('no certificate received') + notafter = cert.get('notAfter') + if notafter and time.time() > ssl.cert_time_to_seconds(notafter): + return _('certificate expired %s') % notafter + notbefore = cert.get('notBefore') + if notbefore and time.time() < ssl.cert_time_to_seconds(notbefore): + return _('certificate not valid before %s') % notbefore + dnsname = hostname.lower() + for s in cert.get('subject', []): + key, value = s[0] + if key == 'commonName': + certname = value.lower() + if (certname == dnsname or + '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]): + return None + return _('certificate is for %s') % certname + return _('no commonName found in certificate') + if has_https: class BetterHTTPS(httplib.HTTPSConnection): send = keepalive.safesend @@ -501,7 +526,11 @@ if has_https: self.sock = _ssl_wrap_socket(sock, self.key_file, self.cert_file, cert_reqs=CERT_REQUIRED, ca_certs=cacerts) - self.ui.debug(_('server identity verification succeeded\n')) + msg = _verifycert(self.sock.getpeercert(), self.host) + if msg: + raise util.Abort('%s certificate error: %s' % (self.host, msg)) + self.ui.debug(_('%s certificate successfully verified\n') % + self.host) else: httplib.HTTPSConnection.connect(self) diff --git a/tests/test-doctest.py b/tests/test-doctest.py --- a/tests/test-doctest.py +++ b/tests/test-doctest.py @@ -5,21 +5,16 @@ if 'TERM' in os.environ: import doctest import mercurial.changelog -# test doctest from changelog - doctest.testmod(mercurial.changelog) -import mercurial.httprepo -doctest.testmod(mercurial.httprepo) - -import mercurial.util -doctest.testmod(mercurial.util) +import mercurial.dagparser +doctest.testmod(mercurial.dagparser, optionflags=doctest.NORMALIZE_WHITESPACE) import mercurial.match doctest.testmod(mercurial.match) -import mercurial.dagparser -doctest.testmod(mercurial.dagparser, optionflags=doctest.NORMALIZE_WHITESPACE) +import mercurial.url +doctest.testmod(mercurial.url) import hgext.convert.cvsps doctest.testmod(hgext.convert.cvsps) diff --git a/tests/test-url.py b/tests/test-url.py new file mode 100644 --- /dev/null +++ b/tests/test-url.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +def check(a, b): + if a != b: + print (a, b) + +from mercurial.url import _verifycert + +# Test non-wildcard certificates +check(_verifycert({'subject': ((('commonName', 'example.com'),),)}, 'example.com'), + None) +check(_verifycert({'subject': ((('commonName', 'example.com'),),)}, 'www.example.com'), + 'certificate is for example.com') +check(_verifycert({'subject': ((('commonName', 'www.example.com'),),)}, 'example.com'), + 'certificate is for www.example.com') + +# Test wildcard certificates +check(_verifycert({'subject': ((('commonName', '*.example.com'),),)}, 'www.example.com'), + None) +check(_verifycert({'subject': ((('commonName', '*.example.com'),),)}, 'example.com'), + 'certificate is for *.example.com') +check(_verifycert({'subject': ((('commonName', '*.example.com'),),)}, 'w.w.example.com'), + 'certificate is for *.example.com') + +# Avoid some pitfalls +check(_verifycert({'subject': ((('commonName', '*.foo'),),)}, 'foo'), + 'certificate is for *.foo') +check(_verifycert({'subject': ((('commonName', '*o'),),)}, 'foo'), + 'certificate is for *o') + +import time +lastyear = time.gmtime().tm_year - 1 +nextyear = time.gmtime().tm_year + 1 +check(_verifycert({'notAfter': 'May 9 00:00:00 %s GMT' % lastyear}, 'example.com'), + 'certificate expired May 9 00:00:00 %s GMT' % lastyear) +check(_verifycert({'notBefore': 'May 9 00:00:00 %s GMT' % nextyear}, 'example.com'), + 'certificate not valid before May 9 00:00:00 %s GMT' % nextyear) +check(_verifycert({'notAfter': 'Sep 29 15:29:48 %s GMT' % nextyear, 'subject': ()}, 'example.com'), + 'no commonName found in certificate') +check(_verifycert(None, 'example.com'), + 'no certificate received')