--- a/doc/hgrc.5.txt Thu Sep 30 13:38:21 2010 +0200
+++ b/doc/hgrc.5.txt Fri Oct 01 00:54:03 2010 +0200
@@ -951,8 +951,9 @@
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 @@
... (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``.
--- a/mercurial/help/urls.txt Thu Sep 30 13:38:21 2010 +0200
+++ b/mercurial/help/urls.txt Fri Oct 01 00:54:03 2010 +0200
@@ -18,6 +18,9 @@
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
--- a/mercurial/url.py Thu Sep 30 13:38:21 2010 +0200
+++ b/mercurial/url.py Fri Oct 01 00:54:03 2010 +0200
@@ -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 @@
_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 @@
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)
--- a/tests/test-doctest.py Thu Sep 30 13:38:21 2010 +0200
+++ b/tests/test-doctest.py Fri Oct 01 00:54:03 2010 +0200
@@ -5,21 +5,16 @@
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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-url.py Fri Oct 01 00:54:03 2010 +0200
@@ -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')