[PATCH 1 of 8] sslutil: introduce a function for determining host-specific settings

Gregory Szorc gregory.szorc at gmail.com
Sat May 28 20:04:23 UTC 2016


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1464459122 25200
#      Sat May 28 11:12:02 2016 -0700
# Node ID 281a76456d7e198949e97612f277e49c93e98b8d
# Parent  9da137faaa9c9593f821923189316e7fcb38cf67
sslutil: introduce a function for determining host-specific settings

This patch marks the beginning of a series that introduces a new,
more configurable, per-host security settings mechanism. Currently,
we have global settings (like web.cacerts and the --insecure argument).
We also have per-host settings via [hostfingerprints].

Global security settings are good for defaults, but they don't
provide the amount of control often wanted. For example, an
organization may want to require a particular CA is used for a
particular hostname.

[hostfingerprints] is nice. But it currently assumes SHA-1.
Furthermore, there is no obvious place to put additional per-host
settings.

Subsequent patches will be introducing new mechanisms for defining
security settings, some on a per-host basis. This commits starts
the transition to that world by introducing the _hostsettings
function. It takes a ui and hostname and returns a dict of security
settings. Currently, it limits itself to returning host fingerprint
info.

We foreshadow the future support of non-SHA1 hashing algorithms
for verifying the host fingerprint by making the "certfingerprints"
key a list of tuples instead of a list of hashes.

We add this dict to the hgstate property on the socket and use it
during socket validation for checking fingerprints. There should be
no change in behavior.

diff --git a/mercurial/sslutil.py b/mercurial/sslutil.py
--- a/mercurial/sslutil.py
+++ b/mercurial/sslutil.py
@@ -101,16 +101,33 @@ except AttributeError:
                 'ca_certs': self._cacerts,
             }
 
             if self._supportsciphers:
                 args['ciphers'] = self._ciphers
 
             return ssl.wrap_socket(socket, **args)
 
+def _hostsettings(ui, hostname):
+    """Obtain security settings for a hostname.
+
+    Returns a dict of settings relevant to that hostname.
+    """
+    s = {
+        # List of 2-tuple of (hash algorithm, hash).
+        'certfingerprints': [],
+    }
+
+    # Fingerprints from [hostfingerprints] are always SHA-1.
+    for fingerprint in ui.configlist('hostfingerprints', hostname, []):
+        fingerprint = fingerprint.replace(':', '').lower()
+        s['certfingerprints'].append(('sha1', fingerprint))
+
+    return s
+
 def _determinecertoptions(ui, host):
     """Determine certificate options for a connections.
 
     Returns a tuple of (cert_reqs, ca_certs).
     """
     # If a host key fingerprint is on file, it is the only thing that matters
     # and CA certs don't come into play.
     hostfingerprint = ui.config('hostfingerprints', host)
@@ -212,16 +229,17 @@ def wrapsocket(sock, keyfile, certfile, 
     # closed
     # - see http://bugs.python.org/issue13721
     if not sslsocket.cipher():
         raise error.Abort(_('ssl connection failed'))
 
     sslsocket._hgstate = {
         'caloaded': caloaded,
         'hostname': serverhostname,
+        'settings': _hostsettings(ui, serverhostname),
         'ui': ui,
     }
 
     return sslsocket
 
 def _verifycert(cert, hostname):
     '''Verify that cert (in socket.getpeercert() format) matches hostname.
     CRLs is not handled.
@@ -287,38 +305,37 @@ def _defaultcacerts():
 
 def validatesocket(sock, strict=False):
     """Validate a socket meets security requiremnets.
 
     The passed socket must have been created with ``wrapsocket()``.
     """
     host = sock._hgstate['hostname']
     ui = sock._hgstate['ui']
+    settings = sock._hgstate['settings']
 
     try:
         peercert = sock.getpeercert(True)
         peercert2 = sock.getpeercert()
     except AttributeError:
         raise error.Abort(_('%s ssl connection error') % host)
 
     if not peercert:
         raise error.Abort(_('%s certificate error: '
                            'no certificate received') % host)
 
     # If a certificate fingerprint is pinned, use it and only it to
     # validate the remote cert.
-    hostfingerprints = ui.configlist('hostfingerprints', host)
     peerfingerprint = util.sha1(peercert).hexdigest()
     nicefingerprint = ":".join([peerfingerprint[x:x + 2]
         for x in xrange(0, len(peerfingerprint), 2)])
-    if hostfingerprints:
+    if settings['certfingerprints']:
         fingerprintmatch = False
-        for hostfingerprint in hostfingerprints:
-            if peerfingerprint.lower() == \
-                    hostfingerprint.replace(':', '').lower():
+        for hash, fingerprint in settings['certfingerprints']:
+            if peerfingerprint.lower() == fingerprint:
                 fingerprintmatch = True
                 break
         if not fingerprintmatch:
             raise error.Abort(_('certificate for %s has unexpected '
                                'fingerprint %s') % (host, nicefingerprint),
                              hint=_('check hostfingerprint configuration'))
         ui.debug('%s certificate matched fingerprint %s\n' %
                  (host, nicefingerprint))


More information about the Mercurial-devel mailing list