[PATCH 2 of 2] sslutil: allow multiple fingerprints per host

Gregory Szorc gregory.szorc at gmail.com
Sun Mar 13 17:05:42 EDT 2016

# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1457903038 25200
#      Sun Mar 13 14:03:58 2016 -0700
# Node ID b445e4f6ae2ffe9b0c045bb9952ba00a9a878386
# Parent  d7b33859bc8f5c74c2d4b1057f00bac8137d6e1c
sslutil: allow multiple fingerprints per host

Certificate pinning via [hostfingerprints] is a useful security
feature. Currently, we only support one fingerprint per hostname.
This is simple but it fails in the real world:

* Switching certificates breaks clients until they change the
  pinned certificate fingerprint. This incurs client downtime
  and can require massive amounts of coordination to perform
  certificate changes.
* Some servers operate with multiple certificates on the same

This patch adds support for defining multiple certificate
fingerprints per host. This overcomes the deficiencies listed
above. I anticipate the primary use case of this feature will
be to define both the old and new certificate so a certificate
transition can occur with minimal interruption, so this scenario
has been called out in the help documentation.

diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
--- a/mercurial/help/config.txt
+++ b/mercurial/help/config.txt
@@ -969,16 +969,19 @@ is treated as a failure.
 Fingerprints of the certificates of known HTTPS servers.
 A HTTPS connection to a server with a fingerprint configured here will
 only succeed if the servers certificate matches the fingerprint.
 This is very similar to how ssh known hosts works.
 The fingerprint is the SHA-1 hash value of the DER encoded certificate.
+Multiple values can be specified (separated by spaces or commas). This can
+be used to define both old and new fingerprints while a host transitions
+to a new certificate.
 The CA chain and web.cacerts is not used for servers with a fingerprint.
 For example::
     hg.intevation.de = fc:e2:8d:d9:51:cd:cb:c1:4d:18:6b:b7:44:8d:49:72:57:e6:cd:33
     hg.intevation.org = fc:e2:8d:d9:51:cd:cb:c1:4d:18:6b:b7:44:8d:49:72:57:e6:cd:33
diff --git a/mercurial/sslutil.py b/mercurial/sslutil.py
--- a/mercurial/sslutil.py
+++ b/mercurial/sslutil.py
@@ -157,35 +157,40 @@ def sslkwargs(ui, host):
 class validator(object):
     def __init__(self, ui, host):
         self.ui = ui
         self.host = host
     def __call__(self, sock, strict=False):
         host = self.host
         cacerts = self.ui.config('web', 'cacerts')
-        hostfingerprint = self.ui.config('hostfingerprints', host)
+        hostfingerprints = self.ui.configlist('hostfingerprints', host)
         if not sock.cipher(): # work around http://bugs.python.org/issue13721
             raise error.Abort(_('%s ssl connection error') % host)
             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)
         peerfingerprint = util.sha1(peercert).hexdigest()
         nicefingerprint = ":".join([peerfingerprint[x:x + 2]
             for x in xrange(0, len(peerfingerprint), 2)])
-        if hostfingerprint:
-            if peerfingerprint.lower() != \
-                    hostfingerprint.replace(':', '').lower():
+        if hostfingerprints:
+            fingerprintmatch = False
+            for hostfingerprint in hostfingerprints:
+                if peerfingerprint.lower() == \
+                        hostfingerprint.replace(':', '').lower():
+                    fingerprintmatch = True
+                    break
+            if not fingerprintmatch:
                 raise error.Abort(_('certificate for %s has unexpected '
                                    'fingerprint %s') % (host, nicefingerprint),
                                  hint=_('check hostfingerprint configuration'))
             self.ui.debug('%s certificate matched fingerprint %s\n' %
                           (host, nicefingerprint))
         elif cacerts != '!':
             msg = _verifycert(peercert2, host)
             if msg:
diff --git a/tests/test-https.t b/tests/test-https.t
--- a/tests/test-https.t
+++ b/tests/test-https.t
@@ -285,16 +285,31 @@ Fingerprints
   $ echo "[hostfingerprints]" >> copy-pull/.hg/hgrc
   $ echo "localhost = 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca" >> copy-pull/.hg/hgrc
   $ echo " = 914f1aff87249c09b6859b88b1906d30756491ca" >> copy-pull/.hg/hgrc
 - works without cacerts
   $ hg -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts=!
+- multiple fingerprints specified and first matches
+  $ hg --config 'hostfingerprints.localhost=914f1aff87249c09b6859b88b1906d30756491ca, deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts=!
+  5fed3813f7f5
+- multiple fingerprints specified and last matches
+  $ hg --config 'hostfingerprints.localhost=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, 914f1aff87249c09b6859b88b1906d30756491ca' -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts=!
+  5fed3813f7f5
+- multiple fingerprints specified and none match
+  $ hg --config 'hostfingerprints.localhost=deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, aeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts=!
+  abort: certificate for localhost has unexpected fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca
+  (check hostfingerprint configuration)
+  [255]
 - fails when cert doesn't match hostname (port is ignored)
   $ hg -R copy-pull id https://localhost:$HGPORT1/
   abort: certificate for localhost has unexpected fingerprint 28:ff:71:bf:65:31:14:23:ad:62:92:b4:0e:31:99:18:fc:83:e3:9b
   (check hostfingerprint configuration)
 - ignores that certificate doesn't match hostname

More information about the Mercurial-devel mailing list