[PATCH stable] https: verify server certificate with cacert when Python doesn't (issue2407)

Mads Kiilerich mads at kiilerich.com
Wed Sep 29 19:00:10 CDT 2010


# HG changeset patch
# User Mads Kiilerich <mads at kiilerich.com>
# Date 1285804727 -7200
# Branch stable
# Node ID 5a222250de197b4343419a4a18d0ee150b5ded03
# Parent  1c9bb7e00f7182198b648d8126406ece67c25899
https: verify server certificate with cacert when Python doesn't (issue2407)

We now explicitly verify that the certificate received for https connections
matches the requested hostname and is valid for the time being.

This is a minimal patch where we try to fail to the safe side, but we do still
rely on Python's SSL functionality and do not try to implement the standards
fully and correctly. CRLs and subjectAltName are not handled and proxies
haven't been considered.

This change might break connections to some sites if cacerts is specified and
the certificates (by our definition) isn't correct. The workaround is to
disable cacerts which in most cases isn't much worse than it was before with
cacerts.

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 @@
     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``.
 
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 @@
 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
@@ -469,6 +469,72 @@
         _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.
+
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', 'example.com'),),)}, 'example.com')
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', 'example.com'),),)}, 'www.example.com')
+    'server certificate error: certificate for example.com from www.example.com'
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', 'www.example.com'),),)}, 'example.com')
+    'server certificate error: certificate for www.example.com from example.com'
+
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', '*.example.com'),),)}, 'www.example.com')
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', '*.example.com'),),)}, 'example.com')
+    'server certificate error: certificate for *.example.com from example.com'
+    >>> _verifycert(
+    ...     {'subject': ((('commonName', '*.example.com'),),)}, 'w.w.example.com')
+    'server certificate error: certificate for *.example.com from w.w.example.com'
+
+    >>> _verifycert(
+    ...     {'notAfter': 'May  9 00:00:00 2007 GMT'}, 'example.com')
+    'server certificate error: certificate expired May  9 00:00:00 2007 GMT'
+    >>> _verifycert(
+    ...     {'notBefore': 'May  9 00:00:00 2037 GMT'}, 'example.com')
+    'server certificate error: certificate from future May  9 00:00:00 2037 GMT'
+    >>> _verifycert(
+    ...     {'notAfter': 'Sep 29 15:29:48 2037 GMT',
+    ...     'subject': ()}, 'example.com')
+    'server certificate error: certificate from example.com not recognized'
+    >>> _verifycert(None, 'example.com')
+    'server certificate error: no certificate received from example.com'
+    """
+    dnsname = hostname.lower()
+    if not cert:
+        return _('server certificate error: no certificate received from %s'
+            ) % dnsname
+    notafter = cert.get('notAfter')
+    if notafter:
+        if time.time() >= ssl.cert_time_to_seconds(notafter):
+            return _('server certificate error: certificate expired %s'
+                ) % notafter
+    notbefore = cert.get('notBefore')
+    if notbefore:
+        if time.time() <= ssl.cert_time_to_seconds(notbefore):
+            return _('server certificate error: certificate from future %s'
+                ) % notbefore
+    for s in cert.get('subject', []):
+        if s[0][0] == 'commonName':
+            certname = s[0][1].lower()
+            if dnsname == certname or (
+                certname.startswith('*.') and
+                dnsname.endswith(certname[1:]) and
+                '.' not in dnsname[:-len(certname) + 1]
+                ):
+                return None
+            return _('server certificate error: certificate for %s from %s'
+                ) % (certname, dnsname)
+    return _('server certificate error: certificate from %s not recognized'
+        ) % dnsname
+
 if has_https:
     class BetterHTTPS(httplib.HTTPSConnection):
         send = keepalive.safesend
@@ -484,7 +550,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(msg)
+                self.ui.debug(_('server certificate for %s 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,13 +5,14 @@
 import doctest
 
 import mercurial.changelog
-# test doctest from changelog
-
 doctest.testmod(mercurial.changelog)
 
 import mercurial.httprepo
 doctest.testmod(mercurial.httprepo)
 
+import mercurial.url
+doctest.testmod(mercurial.url)
+
 import mercurial.util
 doctest.testmod(mercurial.util)
 


More information about the Mercurial-devel mailing list