[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