merge with stable
authorMads Kiilerich <mads@kiilerich.com>
Fri, 01 Oct 2010 00:54:03 +0200
changeset 12595 0f83a402faa0
parent 12591 4b9f23885a55 (current diff)
parent 12594 bb324910e40a (diff)
child 12598 e660e4a178c6
merge with stable
mercurial/url.py
tests/test-doctest.py
--- 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')