[PATCH 1 of 1 RFC] url: debug print ssl certificate info if verify failed

Yuya Nishihara yuya at tcha.org
Sun Jan 9 09:12:00 CST 2011


# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1294583184 -32400
# Node ID 94b670827fba25c056643015f8c7b3c1f52df0f5
# Parent  0c554a73798b08106def4328d68a2053769b5c3e
url: debug print ssl certificate info if verify failed

If a server provides certificate that can't be verified by the configured
cacerts, ssl.wrap_socket raises exception like
"routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed".

This patch tries to show data about unverified certificate, so that a user
can know detailed circumstances.

As of Python 2.6 or 2.7, it doesn't officially provide a way to decode
PEM-encoded certificate to human-readable text.
This patch uses _ssl._test_decode_cert(), which is available at least on
CPython 2.6.6 or 2.7.1.

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, tempfile
 import __builtin__
 from i18n import _
 import keepalive, util
@@ -308,8 +308,11 @@ if has_https:
         import ssl
         _ssl_wrap_socket = ssl.wrap_socket
         CERT_REQUIRED = ssl.CERT_REQUIRED
+        SSLError = ssl.SSLError
     except ImportError:
         CERT_REQUIRED = 2
+        class SSLError(Exception):
+            pass
 
         def _ssl_wrap_socket(sock, key_file, cert_file,
                              cert_reqs=CERT_REQUIRED, ca_certs=None):
@@ -527,6 +530,71 @@ def _verifycert(cert, hostname):
             return _('certificate is for %s') % certname
     return _('no commonName found in certificate')
 
+def _formatcert(cert):
+    """Return formatted text for the given certificate info
+
+    >>> cert = {'issuer': ((('commonName', u'localhost'),),
+    ...                    (('emailAddress', u'hg at localhost'),)),
+    ...         'notAfter': 'Aug 31 12:50:48 2035 GMT',
+    ...         'notBefore': 'Jan  9 12:50:48 2011 GMT',
+    ...         'subject': ((('commonName', u'localhost'),),
+    ...                     (('emailAddress', u'hg at localhost'),))}
+    >>> _formatcert(cert) # doctest: +NORMALIZE_WHITESPACE
+    'ssl certificate:\\n
+      notBefore: Jan  9 12:50:48 2011 GMT\\n
+      notAfter: Aug 31 12:50:48 2035 GMT\\n
+      issuer.commonName: localhost\\n
+      issuer.emailAddress: hg at localhost\\n
+      subject.commonName: localhost\\n
+      subject.emailAddress: hg at localhost\\n'
+    """
+    l = []
+    l.append('ssl certificate:')
+    for k in ('notBefore', 'notAfter'):
+        if k in cert:
+            l.append(' %s: %s' % (k, cert[k]))
+    for k in ('issuer', 'subject'):
+        for s in cert.get(k, []):
+            key, value = s[0]
+            l.append(' %s.%s: %s'
+                     % (k, key, value.encode('ascii', 'replace')))
+    for key, value in cert.get('subjectAltName', []):
+        l.append(' subjectAltName.%s: %s' % (key, value))
+    return '\n'.join(l) + '\n'
+
+def _decodecert(pem):
+    """Decode PEM-encoded string to a dict like a result of getpeercert()"""
+    try:  # needs CPython 2.6 or 2.7
+        import _ssl
+        decode_certfile = _ssl._test_decode_cert
+    except (ImportError, AttributeError):
+        return
+    if not pem:
+        return
+    fh, fn = tempfile.mkstemp(prefix='hg-servercert-')
+    try:
+        f = os.fdopen(fh, 'w')
+        f.write(pem)
+        f.close()
+        return decode_certfile(fn)
+    finally:
+        os.unlink(fn)
+
+def _debugservercert(ui, addr):
+    if not ui.debugflag:
+        return
+    try:
+        import ssl
+    except ImportError:
+        return
+    try:
+        pem = ssl.get_server_certificate(addr)
+    except SSLError:
+        return
+    cert = _decodecert(pem)
+    if cert:
+        ui.debug(_formatcert(cert))
+
 if has_https:
     class BetterHTTPS(httplib.HTTPSConnection):
         send = keepalive.safesend
@@ -541,9 +609,13 @@ if has_https:
 
             if cacerts:
                 sock = _create_connection((self.host, self.port))
-                self.sock = _ssl_wrap_socket(sock, self.key_file,
-                        self.cert_file, cert_reqs=CERT_REQUIRED,
-                        ca_certs=cacerts)
+                try:
+                    self.sock = _ssl_wrap_socket(sock, self.key_file,
+                            self.cert_file, cert_reqs=CERT_REQUIRED,
+                            ca_certs=cacerts)
+                except SSLError:
+                    _debugservercert(self.ui, (self.host, self.port))
+                    raise
                 msg = _verifycert(self.sock.getpeercert(), self.host)
                 if msg:
                     raise util.Abort(_('%s certificate error: %s') %
diff --git a/tests/test-https.t b/tests/test-https.t
--- a/tests/test-https.t
+++ b/tests/test-https.t
@@ -188,3 +188,18 @@ Test server cert which no longer is vali
   $ hg -R copy-pull pull --config web.cacerts=pub-expired.pem https://localhost:$HGPORT2/
   abort: error: *:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed (glob)
   [255]
+
+debug output on certificate verify failed
+
+  $ hg -R copy-pull --debug pull --config web.cacerts=pub-other.pem
+  using https://localhost:$HGPORT/
+  sending between command
+  ssl certificate:
+   notBefore: Oct 14 20:30:14 2010 GMT
+   notAfter: Jun  5 20:30:14 2035 GMT
+   issuer.commonName: localhost
+   issuer.emailAddress: hg at localhost
+   subject.commonName: localhost
+   subject.emailAddress: hg at localhost
+  abort: error: *:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed (glob)
+  [255]


More information about the Mercurial-devel mailing list