Patchwork [3,of,4] debug: add a method to check the state of, and built an SSL cert chain

login
register
mail settings
Submitter Matt Harbison
Date July 13, 2017, 10:40 p.m.
Message ID <f931e230e7265b024e82.1499985607@MATT7H-PC.attotech.com>
Download mbox | patch
Permalink /patch/22304/
State Accepted
Headers show

Comments

Matt Harbison - July 13, 2017, 10:40 p.m.
# HG changeset patch
# User Matt Harbison <matt_harbison@yahoo.com>
# Date 1490848066 14400
#      Thu Mar 30 00:27:46 2017 -0400
# Node ID f931e230e7265b024e823c32b50a3167dd9e43a4
# Parent  0d8d8dffab1d315bbadee6569f28224d6b1f59df
debug: add a method to check the state of, and built an SSL cert chain

This is only useful on Windows, and avoids the need to use Internet Explorer to
build the certificate chain.  I can see this being extended in the future to
print information about the certificate(s) to help debug issues on any platform.
Maybe even perform some of the python checks listed on the secure connections
wiki page.  But for now, all I need is 1) a command that can be invoked in a
setup script to ensure the certificate is installed, and 2) a command that the
user can run if/when a certificate changes in the future.

It would have been nice to leverage the sslutil library to pick up host specific
settings, but attempting to use sslutil.wrapsocket() failed the
'not sslsocket.cipher()' check in it and aborted.

The output is a little more chatty than some commands, but I've seen the update
take 10+ seconds, and this is only a debug command.

Patch

diff --git a/mercurial/debugcommands.py b/mercurial/debugcommands.py
--- a/mercurial/debugcommands.py
+++ b/mercurial/debugcommands.py
@@ -13,6 +13,7 @@  import operator
 import os
 import random
 import socket
+import ssl
 import string
 import sys
 import tempfile
@@ -2057,6 +2058,66 @@  def debugsetparents(ui, repo, rev1, rev2
     with repo.wlock():
         repo.setparents(r1, r2)
 
+@command('debugssl', [], '[SOURCE]', optionalrepo=True)
+def debugssl(ui, repo, source=None, **opts):
+    '''test a secure connection to a server
+
+    This builds the certificate chain for the server on Windows, installing the
+    missing intermediates and trusted root via Windows Update if necessary.  It
+    does nothing on other platforms.
+
+    If SOURCE is omitted, the 'default' path will be used.  If a URL is given,
+    that server is used. See :hg:`help urls` for more information.
+
+    If the update succeeds, retry the original operation.  Otherwise, the cause
+    of the SSL error is likely another issue.
+    '''
+    if pycompat.osname != 'nt':
+        raise error.Abort(_('Certificate chain building is only possible on '
+                            'Windows'))
+
+    if not source:
+        source = "default"
+    elif not repo:
+        raise error.Abort(_("there is no Mercurial repository here, and no "
+                            "server specified"))
+
+    source, branches = hg.parseurl(ui.expandpath(source))
+    url = util.url(source)
+    addr = None
+
+    if url.scheme == 'https':
+        addr = (url.host, url.port or 443)
+    elif url.scheme == 'ssh':
+        addr = (url.host, url.port or 22)
+    else:
+        raise error.Abort(_("Only https and ssh connections are supported"))
+
+    from . import win32
+
+    s = ssl.wrap_socket(socket.socket(), ssl_version=ssl.PROTOCOL_TLS,
+                        cert_reqs=ssl.CERT_NONE, ca_certs=None)
+
+    try:
+        s.connect(addr)
+        cert = s.getpeercert(True)
+
+        ui.status(_('Checking the certificate chain for %s.\n') % url.host)
+
+        complete = win32.checkcertificatechain(cert, build=False)
+
+        if not complete:
+            ui.status(_('The certificate chain is incomplete.  Updating... '))
+
+            if not win32.checkcertificatechain(cert):
+                ui.status(_('Failed.\n'))
+            else:
+                ui.status(_('Done.\n'))
+        else:
+            ui.status(_('The full certificate chain is available.\n'))
+    finally:
+        s.close()
+
 @command('debugsub',
     [('r', 'rev', '',
      _('revision to check'), _('REV'))],
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -108,6 +108,7 @@  Show debug commands if there are no othe
   debugrevlog
   debugrevspec
   debugsetparents
+  debugssl
   debugsub
   debugsuccessorssets
   debugtemplate
@@ -283,6 +284,7 @@  Show all commands + options
   debugrevlog: changelog, manifest, dir, dump
   debugrevspec: optimize, show-revs, show-set, show-stage, no-optimized, verify-optimized
   debugsetparents: 
+  debugssl: 
   debugsub: rev
   debugsuccessorssets: closest
   debugtemplate: rev, define
diff --git a/tests/test-help.t b/tests/test-help.t
--- a/tests/test-help.t
+++ b/tests/test-help.t
@@ -952,6 +952,7 @@  Test list of internal help commands
    debugrevspec  parse and apply a revision specification
    debugsetparents
                  manually set the parents of the current working directory
+   debugssl      test a secure connection to a server
    debugsub      (no help text available)
    debugsuccessorssets
                  show set of successors for revision