[gnome-keysign: 4/8] gpgmeh: raise error if we import a new certificate rather than new sigs




commit a9bc9fdc952dfe27903c2ca66b38ddf312dbe6d3
Author: Tobias Mueller <muelli cryptobitch de>
Date:   Tue Jun 16 09:42:48 2020 +0200

    gpgmeh: raise error if we import a new certificate rather than new sigs
    
    The attack is subtle and maybe not very relevant:
    Two parties have exchanged their certificates and now one side is
    waiting for the email with the newly produced certifications (aka
    "signatures"). The attacker sends a prepared email with a new
    certificate that it wants the victim to import. Maybe to poison the
    keyring or to make the victim look bad by placing some phishy
    certificates.
    
    This change attempts to detect that and raises an error if it thinks it
    is being tricked.

 keysign/gpgmeh.py                  | 100 +++++++++++++++++++++++--------------
 tests/fixtures/third_party.pgp.asc |  81 ++++++++++++++++++++++++++++++
 tests/test_gpgmeh.py               |  69 +++++++++++++++++++++++++
 3 files changed, 213 insertions(+), 37 deletions(-)
---
diff --git a/keysign/gpgmeh.py b/keysign/gpgmeh.py
index 5ba1e05..39b9ce1 100755
--- a/keysign/gpgmeh.py
+++ b/keysign/gpgmeh.py
@@ -27,7 +27,7 @@ import platform
 
 import dbus
 import gpg
-from gpg.constants import PROTOCOL_OpenPGP
+from gpg.constants import PROTOCOL_OpenPGP, IMPORT_NEW, IMPORT_SIG
 from gpg.errors import GPGMEError
 
 
@@ -322,6 +322,36 @@ class TempContextWithAgent(TempContext):
 
 
 
+def import_signature_dbus(signature):
+    "Imports a TPK into the user's keyring by using Seahorse's DBus API"
+    name = "org.gnome.seahorse"
+    path = "/org/gnome/seahorse/keys"
+    bus = dbus.SessionBus()
+    result = []
+
+    proxy = bus.get_object(name, path)
+    iface = "org.gnome.seahorse.KeyService"
+    gpg_iface = dbus.Interface(proxy, iface)
+    payload = base64.b64encode(signature).decode('latin-1')
+    payload = '\n'.join(payload[i:(i + 64)] for i in range(0, len(payload), 64))
+    payload = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" + payload + "\n-----END PGP PUBLIC KEY BLOCK-----"
+    result = gpg_iface.ImportKeys("openpgp", payload)
+    log.debug("Importing via DBus: %r", result)
+
+    return result
+
+def import_signature_gpgme(signature, homedir=None):
+    "Imports an OpenPGP TPK into a keyring via GPGME"
+    ctx = DirectoryContext(homedir)
+    ctx.op_import(signature)
+    result = ctx.op_import_result()
+    if len(result.imports) < 1:
+        raise GPGMEError
+
+    return result
+
+
+
 ##
 ## END OF INTERNAL API
 #####
@@ -548,33 +578,10 @@ def decrypt_and_import_signature(encrypted_sig, homedir=None):
 
 
 
-def import_signature_dbus(signature):
-    "Imports a TPK into the user's keyring by using Seahorse's DBus API"
-    name = "org.gnome.seahorse"
-    path = "/org/gnome/seahorse/keys"
-    bus = dbus.SessionBus()
-    result = []
-
-    proxy = bus.get_object(name, path)
-    iface = "org.gnome.seahorse.KeyService"
-    gpg_iface = dbus.Interface(proxy, iface)
-    payload = base64.b64encode(signature).decode('latin-1')
-    payload = '\n'.join(payload[i:(i + 64)] for i in range(0, len(payload), 64))
-    payload = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" + payload + "\n-----END PGP PUBLIC KEY BLOCK-----"
-    result = gpg_iface.ImportKeys("openpgp", payload)
-    log.debug("Importing via DBus: %r", result)
 
-    return result
+class ImportNewCertificationError(GPGMEError):
+    "The import of a TPK failed, probably due to containing a new certificate rather than new 'signatures'"
 
-def import_signature_gpgme(signature, homedir=None):
-    "Imports an OpenPGP TPK into a keyring via GPGME"
-    ctx = DirectoryContext(homedir)
-    ctx.op_import(signature)
-    result = ctx.op_import_result()
-    if len(result.imports) < 1:
-        raise GPGMEError
-
-    return result
 
 def import_signature(signature, homedir=None):
     """
@@ -587,16 +594,35 @@ def import_signature(signature, homedir=None):
     if that failed, resort to using gpgme directly.
     """
     result = []
-    if not homedir:
-        # If a homedir is requested, we have to use the gpgme API, because we cannot specify a GnuPG keyring 
via DBus
-        try:
-            # Try Seahorse DBus
-            result = import_signature_dbus(signature)
-        except dbus.exceptions.DBusException:
-            log.debug("Seahorse DBus is not available")
 
-    # If Seahorse failed we try op_import
-    if len(result) < 1:
-        result = import_signature_gpgme(signature, homedir=homedir)
+    ctx = TempContextWithAgent(DirectoryContext(homedir=homedir))
+    ctx.op_import(signature)
+    res = ctx.op_import_result()
+    log.debug("ImportSignature: Testing for new certificate: %r", res)
+    imports = res.imports
+    if len(imports) != 1:
+        log.error("We expected to import only one certificate, "
+                  "but it seems we have %d", len(import_))
+        raise ImportNewCertificationError
+    else:
+        import_ = imports[0]
+        if import_.status & gpg.constants.IMPORT_NEW:
+            log.error("We did not expect to import a *new* certificate, "
+                      "but this seems to be new: %r", import_)
+            raise ImportNewCertificationError
+        else:
+            assert import_.status & gpg.constants.IMPORT_SIG
 
-    return result
+            if not homedir:
+                # If a homedir is requested, we have to use the gpgme API, because we cannot specify a GnuPG 
keyring via DBus
+                try:
+                    # Try Seahorse DBus
+                    result = import_signature_dbus(signature)
+                except dbus.exceptions.DBusException:
+                    log.debug("Seahorse DBus is not available")
+
+            # If Seahorse failed we try op_import
+            if len(result) < 1:
+                result = import_signature_gpgme(signature, homedir=homedir)
+
+        return result
diff --git a/tests/fixtures/third_party.pgp.asc b/tests/fixtures/third_party.pgp.asc
new file mode 100644
index 0000000..796668e
--- /dev/null
+++ b/tests/fixtures/third_party.pgp.asc
@@ -0,0 +1,81 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+CxFMI+4PaTXZKi+3fA==
+=WBVz
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/test_gpgmeh.py b/tests/test_gpgmeh.py
index fce322a..516dc58 100644
--- a/tests/test_gpgmeh.py
+++ b/tests/test_gpgmeh.py
@@ -31,11 +31,13 @@ from keysign.gpgmeh import DirectoryContext
 from keysign.gpgmeh import UIDExport
 from keysign.gpgmeh import export_uids
 from keysign.gpgmeh import fingerprint_from_keydata
+from keysign.gpgmeh import import_signature
 from keysign.gpgmeh import openpgpkey_from_data
 from keysign.gpgmeh import get_usable_keys
 from keysign.gpgmeh import get_usable_secret_keys
 from keysign.gpgmeh import get_public_key_data
 from keysign.gpgmeh import sign_keydata_and_encrypt
+from keysign.gpgmeh import ImportNewCertificationError
 
 from keysign.gpgkey import to_valid_utf8_string
 
@@ -401,6 +403,21 @@ def get_signatures_for_uids_on_key(ctx, key):
     return uid_sigs
 
 
+def export_public_key(keydata):
+    "Returns the public portion of the key even if you provide a private key"
+    # This might be a secret key, too, so we import and export to
+    # get hold of the public portion.
+    ctx = TempContext()
+    ctx.op_import(keydata)
+    result = ctx.op_import_result()
+    fpr = result.imports[0].fpr
+    sink = gpg.Data()
+    ctx.op_export(fpr, 0, sink)
+    sink.seek(0, 0)
+    public_key = sink.read()
+    assert len(public_key) > 0
+    return public_key
+
 class TestSignAndEncrypt:
     SENDER_KEY = "seckey-no-pw-1.asc"
     RECEIVER_KEY = "seckey-no-pw-2.asc"
@@ -545,6 +562,58 @@ class TestSignAndEncrypt:
         assert_greater(len(sigs_after), len(sigs_before))
 
 
+
+
+    def test_third_party_key(self):
+        """This test tries to trick the receiver by sending
+        an unrelated key of a third party.
+        The receiver must not import that key.
+        """
+        THIRD_PARTY_KEY = "third_party.pgp.asc"
+        self.third_party_key = get_fixture_file(THIRD_PARTY_KEY)
+        self.key_third_party_homedir = tempfile.mkdtemp()
+        third_party_gpgcmd = ["gpg", "--homedir={}".format(self.key_third_party_homedir)]
+        check_call(third_party_gpgcmd + ["--import", self.third_party_key])
+
+        keydata = open(self.third_party_key, "rb").read()
+        public_third_party_key = export_public_key(keydata)
+        # The "sender" sends its certificate via the app and then receives the email with the certification
+        public_sender_key = export_public_key(open(self.key_sender_key, 'rb'))
+
+
+        sender = DirectoryContext(homedir=self.key_sender_homedir)
+        before = list(sender.keylist())
+        third_party = DirectoryContext(homedir=self.key_third_party_homedir)
+
+        third_party.op_import(public_sender_key)
+        result = third_party.op_import_result()
+        if result.considered != 1 and result.imported != 1:
+            raise ValueError("Expected to load exactly one key. %r", result)
+        else:
+            imports = result.imports
+            assert len(imports) == 1, "Imported %d instead of 1" % len(imports)
+            fpr = result.imports[0].fpr
+            target_key = third_party.get_key(fpr)
+            ciphertext, _, _ = third_party.encrypt(
+                plaintext=public_third_party_key,
+                recipients=[target_key],
+                always_trust=True,
+                sign=False,
+            )
+
+            # Now we have transferred the ciphertext to the victim
+            plaintext, result, vrfy = sender.decrypt(ciphertext)
+            log.debug("Decrypt Result: %r", result)
+            result = assert_raises(ImportNewCertificationError,
+                import_signature,
+                    plaintext,
+                    homedir=self.key_sender_homedir)
+            log.debug("Import result: %s", result)
+
+            after = list(sender.keylist())
+
+            assert_equal(len(before), len(after))
+
 class TestLatin1(TestSignAndEncrypt):
     SENDER_KEY = "seckey-latin1.asc"
     RECEIVER_KEY = "seckey-2.asc"


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]