From b4c82826bb1cddd24189fbccde042711213c6c5f Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 12 Oct 2014 01:36:57 +0300 Subject: [PATCH 001/102] Now it announces the local fingerprint in the TXT record. --- Keyserver.py | 7 +++++-- MainWindow.py | 4 ++-- Sections.py | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Keyserver.py b/Keyserver.py index 316c57d..fdbe605 100755 --- a/Keyserver.py +++ b/Keyserver.py @@ -63,9 +63,10 @@ class ServeKeyThread(Thread): If you want to stop serving, call shutdown(). ''' - def __init__(self, data, port=9001, *args, **kwargs): + def __init__(self, data, fpr, port=9001, *args, **kwargs): '''Initializes the server to serve the data''' self.keydata = data + self.fpr = fpr self.port = port super(ServeKeyThread, self).__init__(*args, **kwargs) self.daemon = True @@ -86,6 +87,7 @@ def start(self, data=None, port=None, *args, **kwargs): tries = 10 + fpr = self.fpr if self.fpr else "FIXME fingerprint" kd = data if data else self.keydata class KeyRequestHandler(KeyRequestHandlerBase): '''You will need to create this during runtime''' @@ -102,10 +104,11 @@ class KeyRequestHandler(KeyRequestHandlerBase): # This is a bit of a hack, it really should be # in some lower layer, such as the place were # the socket is created and listen()ed on. + self.avahi_publisher = ap = AvahiPublisher( service_port = port_i, service_name = 'HTTP Keyserver', - service_txt = 'FIXME fingeprint', #FIXME Fingerprint + service_txt = fpr, # self.keydata is too big for Avahi; it chrashes service_type = '_geysign._tcp', ) diff --git a/MainWindow.py b/MainWindow.py index d7e52f4..24b9c89 100644 --- a/MainWindow.py +++ b/MainWindow.py @@ -109,13 +109,13 @@ def setup_avahi_browser(self): return False - def setup_server(self, keydata=None): + def setup_server(self, keydata=None, fpr=None): self.log.info('Serving now') #self.keyserver = Thread(name='keyserver', # target=Keyserver.serve_key, args=('Foobar',)) #self.keyserver.daemon = True self.log.debug('About to call %r', Keyserver.ServeKeyThread) - self.keyserver = Keyserver.ServeKeyThread(str(keydata)) + self.keyserver = Keyserver.ServeKeyThread(str(keydata), str(fpr)) self.log.info('Starting thread %r', self.keyserver) self.keyserver.start() self.log.info('Finsihed serving') diff --git a/Sections.py b/Sections.py index 8090231..2f91192 100644 --- a/Sections.py +++ b/Sections.py @@ -267,9 +267,10 @@ def on_button_clicked(self, button): keyid = self.last_selected_key.keyid() self.keyring.export_data(fpr=str(keyid), secret=False) keydata = self.keyring.context.stdout + fpr = self.last_selected_key.fpr - self.log.debug("Keyserver switched on") - self.app.setup_server(keydata) + self.log.debug("Keyserver switched on ! Serving key with fpr: %s", fpr) + self.app.setup_server(keydata, fpr) self.backButton.set_sensitive(True) From 3226ced414f06140e1e48fed41ea2f6c65b0ffab Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 12 Oct 2014 02:17:17 +0300 Subject: [PATCH 002/102] Keyserver: added the new fingerprint field to the call to start the keyserver. --- Keyserver.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Keyserver.py b/Keyserver.py index fdbe605..2cec707 100755 --- a/Keyserver.py +++ b/Keyserver.py @@ -174,15 +174,21 @@ def stop_thread(t, seconds=5): if len(sys.argv) >= 2: fname = sys.argv[1] KEYDATA = open(fname, 'r').read() + from monkeysign.gpg import TempKeyring + tmpkeyring = TempKeyring() + if tmpkeyring.import_data(KEYDATA): + fpr = tmpkeyring.get_keys().keys()[0] else: KEYDATA = 'Example data' + fpr = "NO fingerprint" + log.info('loading keydata with fingerprint %s', fpr) if len(sys.argv) >= 3: timeout = int(sys.argv[2]) else: timeout = 5 - t = ServeKeyThread(KEYDATA) + t = ServeKeyThread(KEYDATA, fpr) stop_t = Thread(target=stop_thread, args=(t,timeout)) stop_t.daemon = True t.start() From 561c229751885541df3c0cf84c84a159bbcc428a Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 16 Jun 2015 20:47:16 +0300 Subject: [PATCH 003/102] Added a gpg/gpg.py file and gpg/example.py. The gpg.py file contains some use cases of monkeysign gpg wrapper that we need to replace with pygpgme. We need to gradually replace all functionality of monkeysign' gpg wrapper. In example.py you can find a basic use of pygpgme to encrypt a file, taken from the original pygpgme repository. --- keysign/gpg/example.py | 18 ++++ keysign/gpg/gpg.py | 228 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 keysign/gpg/example.py create mode 100644 keysign/gpg/gpg.py diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py new file mode 100644 index 0000000..5c55e19 --- /dev/null +++ b/keysign/gpg/example.py @@ -0,0 +1,18 @@ +import gpgme +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + +ctx = gpgme.Context() +ctx.armor = True + +# key = ctx.get_key('140162A978431A0258B3EC24E69EEE14181523F4') +key = ctx.get_key('85B731F8B58103FD8BDC773AF442F8BBC7336FDB') + +plain = BytesIO(b'Hello World\n') +cipher = BytesIO() + +ctx.encrypt([key], gpgme.ENCRYPT_ALWAYS_TRUST, plain, cipher) + +print(cipher.getvalue()) \ No newline at end of file diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py new file mode 100644 index 0000000..06ff038 --- /dev/null +++ b/keysign/gpg/gpg.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python + +# FIXME: +# Extract all cases where gpg support comes from monkeysign and +# add them to this file. + +import logging +from string import Template +from tempfile import NamedTemporaryFile +import shutil + +from monkeysign.gpg import Keyring, TempKeyring + +SUBJECT = 'Your signed key $fingerprint' +BODY = '''Hi $uid, + + +I have just signed your key + + $fingerprint + + +Thanks for letting me sign your key! + +-- +GNOME Keys +''' + +log = logging.getLogger() +def UIDExport(uid, keydata): + """Export only the UID of a key. + Unfortunately, GnuPG does not provide smth like + --export-uid-only in order to obtain a UID and its + signatures.""" + tmp = TempKeyring() + # Hm, apparently this needs to be set, otherwise gnupg will issue + # a stray "gpg: checking the trustdb" which confuses the gnupg library + tmp.context.set_option('always-trust') + tmp.import_data(keydata) + for fpr, key in tmp.get_keys(uid).items(): + for u in key.uidslist: + key_uid = u.uid + if key_uid != uid: + log.info('Deleting UID %s from key %s', key_uid, fpr) + tmp.del_uid(fingerprint=fpr, pattern=key_uid) + only_uid = tmp.export_data(uid) + + return only_uid + + +def MinimalExport(keydata): + '''Returns the minimised version of a key + + For now, you must provide one key only.''' + tmpkeyring = TempKeyring() + ret = tmpkeyring.import_data(keydata) + log.debug("Returned %s after importing %s", ret, keydata) + assert ret + tmpkeyring.context.set_option('export-options', 'export-minimal') + keys = tmpkeyring.get_keys() + log.debug("Keys after importing: %s (%s)", keys, keys.items()) + # We assume the keydata to contain one key only + fingerprint, key = keys.items()[0] + stripped_key = tmpkeyring.export_data(fingerprint) + return stripped_key + + +class TempKeyringCopy(TempKeyring): + """A temporary keyring which uses the secret keys of a parent keyring + + It mainly copies the public keys from the parent keyring to this temporary + keyring and sets this keyring up such that it uses the secret keys of the + parent keyring. + """ + def __init__(self, keyring, *args, **kwargs): + self.keyring = keyring + # Not a new style class... + if issubclass(self.__class__, object): + super(TempKeyringCopy, self).__init__(*args, **kwargs) + else: + TempKeyring.__init__(self, *args, **kwargs) + + self.log = logging.getLogger() + + tmpkeyring = self + # Copy and paste job from monkeysign.ui.prepare + tmpkeyring.context.set_option('secret-keyring', keyring.homedir + '/secring.gpg') + + # copy the gpg.conf from the real keyring + try: + from_ = keyring.homedir + '/gpg.conf' + to_ = tmpkeyring.homedir + shutil.copy(from_, to_) + self.log.debug('copied your gpg.conf from %s to %s', from_, to_) + except IOError as e: + # no such file or directory is alright: it means the use + # has no gpg.conf (because we are certain the temp homedir + # exists at this point) + if e.errno != 2: + pass + + + # Copy the public parts of the secret keys to the tmpkeyring + signing_keys = [] + for fpr, key in keyring.get_keys(None, secret=True, public=False).items(): + if not key.invalid and not key.disabled and not key.expired and not key.revoked: + signing_keys.append(key) + tmpkeyring.import_data (keyring.export_data (fpr)) + + +def use_case_import_data(keyring, data): + if keyring.import_data(data): + imported_key_fpr = keyring.get_keys().keys()[0] + print imported_key_fpr + + else: + print "Failed to import data" + + +def use_case_export_data(keyring, keyid): + # keyring is a monkeysign.gpg.Keyring object + keys = keyring.get_keys(keyid) + key = keys.values()[0] + + # key is a monkeysign.gpg.OpenPGPkey object + keyid = key.keyid() + fpr = key.fpr + + keyring.export_data(fpr, secret=False) + keydata = keyring.context.stdout + + +def use_case_sign_key(keyring, data, fingerprint): + keyring.context.set_option('export-options', 'export-minimal') + + tmpkeyring = TempKeyringCopy(keyring) + keydata = data + + if keydata: + stripped_key = MinimalExport(keydata) + else: + log.debug("looking for key %s in your keyring", fingerprint) + keyring.context.set_option('export-options', 'export-minimal') + stripped_key = keyring.export_data(fingerprint) + + log.debug('Trying to import key\n%s', stripped_key) + + if tmpkeyring.import_data(stripped_key): + keys = tmpkeyring.get_keys(fingerprint) + log.info("Found keys %s for fp %s", keys, fingerprint) + assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) + key = keys[fingerprint] + uidlist = key.uidslist + + # FIXME: For now, we sign all UIDs. This is bad. + ret = tmpkeyring.sign_key(uidlist[0].uid, signall=True) + log.info("Result of signing %s on key %s: %s", uidlist[0].uid, fingerprint, ret) + + for uid in uidlist: + uid_str = uid.uid + log.info("Processing uid %s %s", uid, uid_str) + + # 3.2. export and encrypt the signature + # 3.3. mail the key to the user + signed_key = UIDExport(uid_str, tmpkeyring.export_data(uid_str)) + log.info("Exported %d bytes of signed key", len(signed_key)) + # self.signui.tmpkeyring.context.set_option('armor') + tmpkeyring.context.set_option('always-trust') + encrypted_key = tmpkeyring.encrypt_data(data=signed_key, recipient=uid_str) + + keyid = str(key.keyid()) + ctx = { + 'uid' : uid_str, + 'fingerprint': fingerprint, + 'keyid': keyid, + } + # We could try to dir=tmpkeyring.dir + # We do not use the with ... as construct as the + # tempfile might be deleted before the MUA had the chance + # to get hold of it. + # Hence we reference the tmpfile and hope that it will be properly + # cleaned up when this object will be destroyed... + tmpfile = NamedTemporaryFile(prefix='gnome-keysign-', suffix='.asc') + self.tmpfiles.append(tmpfile) + filename = tmpfile.name + log.info('Writing keydata to %s', filename) + tmpfile.write(encrypted_key) + # Interesting, sometimes it would not write the whole thing out, + # so we better flush here + tmpfile.flush() + # As we're done with the file, we close it. + #tmpfile.close() + + subject = Template(SUBJECT).safe_substitute(ctx) + body = Template(BODY).safe_substitute(ctx) + self.email_file (to=uid_str, subject=subject, + body=body, files=[filename]) + + +# +# FIXME: replace the following globals with the proper data of your testing keys +# + +# the last 8 characters of the test key fingerprint +keyid = '181523F4' + +# public test key data to import +keydata = ''' +BF532A1C50FCCF59AF131C884D904D5F77D936CD807B6D1D5D23F55A2DF76C2CB325DF1E76757A53664D22C885ECD5569460F5F364407546FA7A09070B3B3FCCBFEAC92D713A42EAD73D42AD857A889C91CA9B9C7F4F3FCB16432EBF +''' +# fingerprint of the test key to sign +fingerprint = '140162A978431A0258B3EC24E69EEE14181523F4' + +def main(): + # These are a couple of use case scenarios for monkeysign' gpg wrapper + # that we need to redo using pygpgme + + keyring = Keyring() + tempkeyring = TempKeyringCopy(keyring) + + use_case_import_data(tempkeyring, keydata) + use_case_export_data(tempkeyring, keyid) + use_case_sign_key(tempkeyring, keydata, fingerprint) + + + +if __name__ == '__main__': + main() \ No newline at end of file From 3049d125af62de8363578e4ed35b5aa3a3a3fc65 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 21 Jun 2015 03:01:08 +0300 Subject: [PATCH 004/102] gpg.py: Added functions for gpgme support on key import/export. I have set up a simple scenario to test current and future replacement functions for the old monkeysign gpg wrapper. Also added a sample keys/key1.pub key. --- keysign/gpg/gpg.py | 90 ++++++++++++++++++++++++++++++++------- keysign/gpg/keys/key1.pub | 27 ++++++++++++ 2 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 keysign/gpg/keys/key1.pub diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 06ff038..0d8ca31 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -6,8 +6,11 @@ import logging from string import Template -from tempfile import NamedTemporaryFile + +import os import shutil +import tempfile +from tempfile import NamedTemporaryFile from monkeysign.gpg import Keyring, TempKeyring @@ -197,24 +200,14 @@ def use_case_sign_key(keyring, data, fingerprint): body=body, files=[filename]) -# -# FIXME: replace the following globals with the proper data of your testing keys -# - -# the last 8 characters of the test key fingerprint -keyid = '181523F4' - -# public test key data to import -keydata = ''' -BF532A1C50FCCF59AF131C884D904D5F77D936CD807B6D1D5D23F55A2DF76C2CB325DF1E76757A53664D22C885ECD5569460F5F364407546FA7A09070B3B3FCCBFEAC92D713A42EAD73D42AD857A889C91CA9B9C7F4F3FCB16432EBF -''' -# fingerprint of the test key to sign -fingerprint = '140162A978431A0258B3EC24E69EEE14181523F4' - -def main(): +def use_case_main(): # These are a couple of use case scenarios for monkeysign' gpg wrapper # that we need to redo using pygpgme + keyid = '181523F4' + keydata = 'Example data' + fingerprint = '140162A978431A0258B3EC24E69EEE14181523F4' + keyring = Keyring() tempkeyring = TempKeyringCopy(keyring) @@ -223,6 +216,71 @@ def main(): use_case_sign_key(tempkeyring, keydata, fingerprint) +############################################################################## +### This part is where we are replacing the above calls to monkeysign API with +### gpgme calls +############################################################################## + +import gpgme +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO + + +keydir = os.path.join(os.path.dirname(__file__), 'keys') +test_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' + + +_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') +gpg_conf_contents = '' + +ctx = gpgme.Context() + +def set_up(): + os.environ['GNUPGHOME'] = _gpghome + fd = open(os.path.join(_gpghome, 'gpg.conf'), 'wb') + fd.write(gpg_conf_contents.encode('UTF-8')) + fd.close() + + +def tear_down(): + del os.environ['GNUPGHOME'] + shutil.rmtree(_gpghome, ignore_errors=True) + + +def keyfile(key): + return open(os.path.join(keydir, key), 'rb') + + +def import_data(keydata): + result = ctx.import_(keydata) + return result + + +def export_key(fpr, armor=True): + ctx.armor = armor + keydata = BytesIO() + ctx.export(fpr, keydata) + return keydata + + +def main(): + # set up the environment + set_up() + + # test import + with keyfile('key1.pub') as fp: + result = import_data(fp) + assert result.imports[0] == (test_fpr, None, gpgme.IMPORT_NEW), "Fail on import" + + # test export + keydata = export_key(result.imports[0][0]) + assert keydata.getvalue().startswith( + b'-----BEGIN PGP PUBLIC KEY BLOCK-----\n'), "Fail on export" + + # clean testing environment + tear_down() if __name__ == '__main__': main() \ No newline at end of file diff --git a/keysign/gpg/keys/key1.pub b/keysign/gpg/keys/key1.pub new file mode 100644 index 0000000..8f18098 --- /dev/null +++ b/keysign/gpg/keys/key1.pub @@ -0,0 +1,27 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.14 (GNU/Linux) + +mI0EU6hgvAEEAMSNolGLgpPJQxLGTTn/Fr+RsWBXeOZCkMg5Bd/V0vuZNYCDKL9T +KhxQ/M9ZrxMciE2QTV932TbNgHttHV0j9Vot92wssyXfHnZ1elNmTSLIhezVVpRg +9fNkQHVG+noJBws7P8y/6sktcTpC6tc9Qq2FeoickcqbnH9PP8sWQy6/ABEBAAG0 +W0FuZHJlaSBHYWJyaWVsIChUaGlzIGlzIGEgdGVtcG9yYXJ5IGtleSwgb25seSBm +b3IgdGVzdGluZyBwdXJwb3NlLikgPGtoZW9wc190aGUxQHlhaG9vLmNvbT6IuAQT +AQIAIgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAlWF6/AACgkQ5p7uFBgV +I/RQFQQAu6niOcBSKl8qtDbb+cGnsxU0QsjI3mMR4cdaoWXyruSzDllIGEAuV2rh +yjn2DfOBhiZZWzqgnw2PB8UM4JvbS+xEthaRoNRUYnQ+pE94ftvjDrCtAZCvAq0y +szD1dCfNmI9WJp048L8nlR2nxILejJVtgNOC18LMmGbizcUZsVmJARwEEAECAAYF +AlOoZqMACgkQSwMtPe2DEqJozQf/Q2lvY6qeVHTj6lsj/SyLhgKsZ5aGzBNUpWLp +0anP8EvFdQGM5JxN5hyV5oRzS2LsNJyV4S9NWVt3q6Xwu6/mA4P4cCwP9RXP6osZ +7uGWnTs+p+MECimd+rCoquwltRJs8JmQVNXQS55bcNRGfTc8OBYhdaXSdVKGSS5A +4y/spmxX0QEAIgbqzxmyz90LfEvyQcFOPwD4U2U5FmEFVmBxV2GTyRSv2ol/59af +RT0xItyTmJ/MWgVPy5UMwxlW6dIO5A9zeStR2S0VDe/LEZr/PhY2Ib6qwV4MhPZ7 +thLjOfN4GAvS7Lxi1JH4phe2U8cGyJCGwnBwpkV4hgFcZBTGdriNBFOoYLwBBACr +XuaBpuVeuhfQ4YksW3vpYS+aI0pryIIG+xQ07U0O2PJwmakKcZCIxvI/D8d/vQGH ++MjgHYw/LWyoQgF0nt5RdKEuV+n2CwgiihCzT4DtnUJVhcXiBSmTS3zsoSsENuy8 +qgJZfTqsk8VtN+iyGhVBvHh4GplAy4VEbn0FuaCcwQARAQABiJ8EGAECAAkCGwwF +AlWF7FIACgkQ5p7uFBgVI/RyXgP5AWZhQhhmnPPh+TSiIeYK8xbJPAEcU076aYQv +8B4UlooZQo3jakoEpdFprGCR//bCLLDhKpUA3/3o1VmGo54LCbm2y/U1UC2m70fm +ZmfR1DbkE06FM0GR9M/5r6vwyNZ3TFwiwa5zk6HsMHxw4ck5J14n3DDPw9V6hMNX +ieS7wgs= +=GEgu +-----END PGP PUBLIC KEY BLOCK----- From be4dacba0716fef6a3af5a552388768ddee66199 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 22 Jun 2015 00:07:54 +0300 Subject: [PATCH 005/102] Added more functions with gpg calls into gpg.py. I am still now sure how to do the transition from monekeysign' gpg wrapper to pygpgme. There are so many calls to gpg from almost every class in Section.py, SignPages.py modules, and many are using the monkeysign.gpg.Keyring class methods or monkeysign.gpg.Context. For now, I have moved the gpgme implementation from gpg.py to examply.py file. --- keysign/gpg/example.py | 75 ++++++++++++++-- keysign/gpg/gpg.py | 190 ++++++++++++++++++++--------------------- 2 files changed, 160 insertions(+), 105 deletions(-) diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py index 5c55e19..e7060c5 100644 --- a/keysign/gpg/example.py +++ b/keysign/gpg/example.py @@ -1,18 +1,75 @@ -import gpgme +#!/usr/bin/env python + +import os +import shutil +import tempfile +import logging + try: from io import BytesIO except ImportError: from StringIO import StringIO as BytesIO -ctx = gpgme.Context() -ctx.armor = True +import gpgme + + +log = logging.getLogger() + +keydir = os.path.join(os.path.dirname(__file__), 'keys') +test_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' + + +_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') + +def set_up_temp_dir(): + os.environ['GNUPGHOME'] = _gpghome + # Copy secrets from .gnupg to temporary dir + try: + from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' + to_ = _gpghome + shutil.copy(from_, to_) + log.debug('copied your gpg.conf from %s to %s', from_, to_) + except IOError as e: + log.error('User has no gpg.conf file') + +def remove_temp_dir(): + del os.environ['GNUPGHOME'] + shutil.rmtree(_gpghome, ignore_errors=True) + + +def keyfile(key): + return open(os.path.join(keydir, key), 'rb') + + +def import_data(context, keydata): + result = context.import_(keydata) + return result + + +def export_key(context, fpr, armor=True): + context.armor = armor + keydata = BytesIO() + context.export(fpr, keydata) + return keydata + + +def main(): + # set up the environment + set_up_temp_dir() + ctx = gpgme.Context() -# key = ctx.get_key('140162A978431A0258B3EC24E69EEE14181523F4') -key = ctx.get_key('85B731F8B58103FD8BDC773AF442F8BBC7336FDB') + # test key import + with keyfile('key1.pub') as fp: + result = import_data(ctx, fp) + assert result.imports[0] == (test_fpr, None, gpgme.IMPORT_NEW), "Fail on import" -plain = BytesIO(b'Hello World\n') -cipher = BytesIO() + # test key export + keydata = export_key(ctx, result.imports[0][0]) + assert keydata.getvalue().startswith( + b'-----BEGIN PGP PUBLIC KEY BLOCK-----\n'), "Fail on export" -ctx.encrypt([key], gpgme.ENCRYPT_ALWAYS_TRUST, plain, cipher) + # clean testing environment + remove_temp_dir() -print(cipher.getvalue()) \ No newline at end of file +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 0d8ca31..a697fff 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -1,35 +1,22 @@ #!/usr/bin/env python # FIXME: -# Extract all cases where gpg support comes from monkeysign and +# Extract all use cases where gpg support comes from monkeysign and # add them to this file. import logging from string import Template -import os import shutil -import tempfile from tempfile import NamedTemporaryFile from monkeysign.gpg import Keyring, TempKeyring -SUBJECT = 'Your signed key $fingerprint' -BODY = '''Hi $uid, +log = logging.getLogger() -I have just signed your key - - $fingerprint - - -Thanks for letting me sign your key! - --- -GNOME Keys -''' +### From Sections.py ### -log = logging.getLogger() def UIDExport(uid, keydata): """Export only the UID of a key. Unfortunately, GnuPG does not provide smth like @@ -68,6 +55,22 @@ def MinimalExport(keydata): return stripped_key +class KeyringCopy(Keyring): + """A Keyring class copy which is only for compatibility + + This class will be the only way to instantiate monkeysign.gpg.Keyring. + """ + def __init__(self, *args, **kwargs): + # Not a new style class... + if issubclass(self.__class__, object): + super(KeyringCopy, self).__init__(*args, **kwargs) + else: + Keyring.__init__(self, *args, **kwargs) + + self.log = loggin.getLogger() + + + class TempKeyringCopy(TempKeyring): """A temporary keyring which uses the secret keys of a parent keyring @@ -111,29 +114,20 @@ def __init__(self, keyring, *args, **kwargs): tmpkeyring.import_data (keyring.export_data (fpr)) -def use_case_import_data(keyring, data): - if keyring.import_data(data): - imported_key_fpr = keyring.get_keys().keys()[0] - print imported_key_fpr - - else: - print "Failed to import data" - +## Monkeypatching to get more debug output +import monkeysign.gpg +bc = monkeysign.gpg.Context.build_command +def build_command(*args, **kwargs): + ret = bc(*args, **kwargs) + #log.info("Building command %s", ret) + log.debug("Building cmd: %s", ' '.join(["'%s'" % c for c in ret])) + return ret +monkeysign.gpg.Context.build_command = build_command -def use_case_export_data(keyring, keyid): - # keyring is a monkeysign.gpg.Keyring object - keys = keyring.get_keys(keyid) - key = keys.values()[0] - # key is a monkeysign.gpg.OpenPGPkey object - keyid = key.keyid() - fpr = key.fpr - keyring.export_data(fpr, secret=False) - keydata = keyring.context.stdout - -def use_case_sign_key(keyring, data, fingerprint): +def sign_key_async(keyring, data, fingerprint): keyring.context.set_option('export-options', 'export-minimal') tmpkeyring = TempKeyringCopy(keyring) @@ -200,87 +194,91 @@ def use_case_sign_key(keyring, data, fingerprint): body=body, files=[filename]) -def use_case_main(): - # These are a couple of use case scenarios for monkeysign' gpg wrapper - # that we need to redo using pygpgme - - keyid = '181523F4' - keydata = 'Example data' - fingerprint = '140162A978431A0258B3EC24E69EEE14181523F4' - - keyring = Keyring() - tempkeyring = TempKeyringCopy(keyring) - - use_case_import_data(tempkeyring, keydata) - use_case_export_data(tempkeyring, keyid) - use_case_sign_key(tempkeyring, keydata, fingerprint) +### From SignPages.py ### +def parse_sig_list(text): + '''Parses GnuPG's signature list (i.e. list-sigs) -############################################################################## -### This part is where we are replacing the above calls to monkeysign API with -### gpgme calls -############################################################################## + The format is described in the GnuPG man page''' + sigslist = [] + for block in text.split("\n"): + if block.startswith("sig"): + record = block.split(":") + log.debug("sig record (%d) %s", len(record), record) + keyid, timestamp, uid = record[4], record[5], record[9] + sigslist.append((keyid, timestamp, uid)) -import gpgme -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO + return sigslist -keydir = os.path.join(os.path.dirname(__file__), 'keys') -test_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' +# This is a cache for a keyring object, so that we do not need +# to create a new object every single time we parse signatures +_keyring = None +def signatures_for_keyid(keyid, keyring=None): + '''Returns the list of signatures for a given key id + This will call out to GnuPG list-sigs, using Monkeysign, + and parse the resulting string into a list of signatures. -_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') -gpg_conf_contents = '' + A default Keyring will be used unless you pass an instance + as keyring argument. + ''' + # Retrieving a cached instance of a keyring, + # unless we were being passed a keyring + global _keyring + if keyring is not None: + kr = keyring + else: + if _keyring is None: + _keyring = Keyring() + kr = _keyring -ctx = gpgme.Context() + # FIXME: this would be better if it was done in monkeysign + kr.context.call_command(['list-sigs', keyid]) + siglist = parse_sig_list(kr.context.stdout) -def set_up(): - os.environ['GNUPGHOME'] = _gpghome - fd = open(os.path.join(_gpghome, 'gpg.conf'), 'wb') - fd.write(gpg_conf_contents.encode('UTF-8')) - fd.close() + return siglist -def tear_down(): - del os.environ['GNUPGHOME'] - shutil.rmtree(_gpghome, ignore_errors=True) +### Below are wrappers for gpg use cases that might be good to have in our +### implementation -def keyfile(key): - return open(os.path.join(keydir, key), 'rb') +def keyring_set_option(keyring, option, value = None): + try: + if option in keyring.context.options: + keyring.context.set_option(option, value) + except AttributeError: + log.error("Object %s has no attribute context", keyring) + except TypeError: + log.error("Object %s is not a Keyring type", keyring) -def import_data(keydata): - result = ctx.import_(keydata) - return result +def keyring_call_command(keyring, command, stdin = None): + try: + if option in keyring.context.options: + keyring.context.call_command(command, stdin) -def export_key(fpr, armor=True): - ctx.armor = armor - keydata = BytesIO() - ctx.export(fpr, keydata) - return keydata + except AttributeError: + log.error("Object %s has no attribute context", keyring) + except TypeError: + log.error("Object %s is not a Keyring type", keyring) -def main(): - # set up the environment - set_up() +def keyring_import_data(keyring, data): + if keyring.import_data(data): + imported_key_fpr = keyring.get_keys().keys()[0] + log.debug("Imported data with fpr:\n%s", imported_key_fpr) - # test import - with keyfile('key1.pub') as fp: - result = import_data(fp) - assert result.imports[0] == (test_fpr, None, gpgme.IMPORT_NEW), "Fail on import" + else: + log.error("Couldn't import data:\n%s", data) - # test export - keydata = export_key(result.imports[0][0]) - assert keydata.getvalue().startswith( - b'-----BEGIN PGP PUBLIC KEY BLOCK-----\n'), "Fail on export" - # clean testing environment - tear_down() +def keyring_export_data(keyring, keyid): + keys = keyring.get_keys(keyid) + key = keys.values()[0] -if __name__ == '__main__': - main() \ No newline at end of file + keyring.export_data(key.fpr, secret=False) + keydata = keyring.context.stdout + return keydata From cf501e59ce56d74ac98e1d9d9a7815eaa58c2594 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 12:46:31 +0300 Subject: [PATCH 006/102] Replaced KeyringCopy class definition with a method that returns a Keyring obj. --- keysign/gpg/gpg.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a697fff..6ea7ab4 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -55,20 +55,8 @@ def MinimalExport(keydata): return stripped_key -class KeyringCopy(Keyring): - """A Keyring class copy which is only for compatibility - - This class will be the only way to instantiate monkeysign.gpg.Keyring. - """ - def __init__(self, *args, **kwargs): - # Not a new style class... - if issubclass(self.__class__, object): - super(KeyringCopy, self).__init__(*args, **kwargs) - else: - Keyring.__init__(self, *args, **kwargs) - - self.log = loggin.getLogger() - +def GetKeyringCopy(): + return Keyring() class TempKeyringCopy(TempKeyring): @@ -282,3 +270,8 @@ def keyring_export_data(keyring, keyid): keyring.export_data(key.fpr, secret=False) keydata = keyring.context.stdout return keydata + + +if __name__ == '__main__': + keyring = GetKeyringCopy() + print keyring.get_keys() \ No newline at end of file From 9182c866ad871ce69bab7a2584284c0056a32b7d Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 13:37:29 +0300 Subject: [PATCH 007/102] Sections.py: Moved global functions that calls gpg into gpg.py. There is still work to be done to separate the gpg calls from whithin core classes in Sections.py. --- Makefile | 2 +- keysign/Sections.py | 104 ++-------------------------------------- keysign/gpg/__init__.py | 1 + setup.py | 15 +++--- 4 files changed, 13 insertions(+), 109 deletions(-) create mode 100644 keysign/gpg/__init__.py diff --git a/Makefile b/Makefile index 7ab1bad..e135b0d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,3 @@ clean: - rm -f *.pyc + find . -name "*.pyc" -type f -delete diff --git a/keysign/Sections.py b/keysign/Sections.py index 699f8d7..d161f8c 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -33,6 +33,8 @@ from monkeysign.ui import MonkeysignUi from monkeysign.gpg import GpgRuntimeError +from keysign.gpg.gpg import UIDExport, MinimalExport, GetKeyringCopy, TempKeyringCopy + from compat import gtkbutton import Keyserver from KeysPage import KeysPage @@ -49,7 +51,6 @@ # Needed for window.get_xid(), xvimagesink.set_window_handle(), respectively: from gi.repository import GstVideo -import key Gst.init([]) @@ -78,105 +79,6 @@ -# FIXME: This probably wants to go somewhere more central. -# Maybe even into Monkeysign. -log = logging.getLogger() -def UIDExport(uid, keydata): - """Export only the UID of a key. - Unfortunately, GnuPG does not provide smth like - --export-uid-only in order to obtain a UID and its - signatures.""" - tmp = TempKeyring() - # Hm, apparently this needs to be set, otherwise gnupg will issue - # a stray "gpg: checking the trustdb" which confuses the gnupg library - tmp.context.set_option('always-trust') - tmp.import_data(keydata) - for fpr, key in tmp.get_keys(uid).items(): - for u in key.uidslist: - key_uid = u.uid - if key_uid != uid: - log.info('Deleting UID %s from key %s', key_uid, fpr) - tmp.del_uid(fingerprint=fpr, pattern=key_uid) - only_uid = tmp.export_data(uid) - - return only_uid - - -def MinimalExport(keydata): - '''Returns the minimised version of a key - - For now, you must provide one key only.''' - tmpkeyring = TempKeyring() - ret = tmpkeyring.import_data(keydata) - log.debug("Returned %s after importing %s", ret, keydata) - assert ret - tmpkeyring.context.set_option('export-options', 'export-minimal') - keys = tmpkeyring.get_keys() - log.debug("Keys after importing: %s (%s)", keys, keys.items()) - # We assume the keydata to contain one key only - fingerprint, key = keys.items()[0] - stripped_key = tmpkeyring.export_data(fingerprint) - return stripped_key - - - -class TempKeyringCopy(TempKeyring): - """A temporary keyring which uses the secret keys of a parent keyring - - It mainly copies the public keys from the parent keyring to this temporary - keyring and sets this keyring up such that it uses the secret keys of the - parent keyring. - """ - def __init__(self, keyring, *args, **kwargs): - self.keyring = keyring - # Not a new style class... - if issubclass(self.__class__, object): - super(TempKeyringCopy, self).__init__(*args, **kwargs) - else: - TempKeyring.__init__(self, *args, **kwargs) - - self.log = logging.getLogger() - - tmpkeyring = self - # Copy and paste job from monkeysign.ui.prepare - tmpkeyring.context.set_option('secret-keyring', keyring.homedir + '/secring.gpg') - - # copy the gpg.conf from the real keyring - try: - from_ = keyring.homedir + '/gpg.conf' - to_ = tmpkeyring.homedir - shutil.copy(from_, to_) - self.log.debug('copied your gpg.conf from %s to %s', from_, to_) - except IOError as e: - # no such file or directory is alright: it means the use - # has no gpg.conf (because we are certain the temp homedir - # exists at this point) - if e.errno != 2: - pass - - - # Copy the public parts of the secret keys to the tmpkeyring - signing_keys = [] - for fpr, key in keyring.get_keys(None, secret=True, public=False).items(): - if not key.invalid and not key.disabled and not key.expired and not key.revoked: - signing_keys.append(key) - tmpkeyring.import_data (keyring.export_data (fpr)) - - - -## Monkeypatching to get more debug output -import monkeysign.gpg -bc = monkeysign.gpg.Context.build_command -def build_command(*args, **kwargs): - ret = bc(*args, **kwargs) - #log.info("Building command %s", ret) - log.debug("Building cmd: %s", ' '.join(["'%s'" % c for c in ret])) - return ret -monkeysign.gpg.Context.build_command = build_command - - - - class KeySignSection(Gtk.VBox): def __init__(self, app): @@ -243,7 +145,7 @@ def on_key_selected(self, pane, keyid): to a key, i.e. the user has made a selection and wants to advance the program. ''' - log.debug('User selected key %s', keyid) + self.log.info('User selected key %s', keyid) key = self.keyring.get_keys(keyid).values()[0] diff --git a/keysign/gpg/__init__.py b/keysign/gpg/__init__.py new file mode 100644 index 0000000..fef66b5 --- /dev/null +++ b/keysign/gpg/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python \ No newline at end of file diff --git a/setup.py b/setup.py index c9ec051..90e8871 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,8 @@ packages = [ 'keysign', 'keysign.compat', - 'keysign.network'], + 'keysign.network', + 'keysign.gpg'], #package_dir={'keysign': 'keysign'}, #package_data={'keysign': ['data/']}, data_files=[ @@ -35,7 +36,7 @@ # to not confuse Ubuntu 14.04's pip as that # seems incompatible with a newer requests library. # https://bugs.launchpad.net/ubuntu/+source/python-pip/+bug/1306991 - 'requests<=2.2', + 'requests<=2.2', 'qrencode', #'monkeysign', # Apparently not in the cheeseshop # avahi # Also no entry in the cheeseshop @@ -43,7 +44,7 @@ ], license='GPLv3+', long_description=open('README.rst').read(), - + entry_points = { #'console_scripts': [ # 'keysign = keysign.main' @@ -53,18 +54,18 @@ 'gks-qrcode = keysign.GPGQRCode:main', ], }, - + classifiers = [ # Maybe not yet... #'Development Status :: 4 - Beta', - + 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Information Technology', 'Intended Audience :: Legal Industry', - 'Intended Audience :: Telecommunications Industry', - + 'Intended Audience :: Telecommunications Industry', + 'Programming Language :: Python', 'Programming Language :: Python :: 2', # I think we are only 2.7 compatible From 6a723a544acf6b397001782a5aa24b07f7035cc9 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 13:58:06 +0300 Subject: [PATCH 008/102] Moved all gpg calls from Sections.py to gpg.py. For now, only Sections.py module interacts with gpg through gpg.py file. --- keysign/Sections.py | 11 ++++------- keysign/gpg/gpg.py | 4 +++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index d161f8c..f8006fd 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -29,11 +29,8 @@ import sys -from monkeysign.gpg import Keyring, TempKeyring from monkeysign.ui import MonkeysignUi -from monkeysign.gpg import GpgRuntimeError - -from keysign.gpg.gpg import UIDExport, MinimalExport, GetKeyringCopy, TempKeyringCopy +from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, GetNewTempKeyring, TempKeyringCopy from compat import gtkbutton import Keyserver @@ -93,7 +90,7 @@ def __init__(self, app): self.app = app self.log = logging.getLogger() - self.keyring = Keyring() + self.keyring = GetNewKeyring() # these are needed later when we need to get details about # a selected key @@ -350,7 +347,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) #FIXME: should we create a new TempKeyring for each key we want # to sign it ? - self.tmpkeyring = TempKeyring() + self.tmpkeyring = GetNewTempKeyring() other_clients = self.sort_clients(other_clients, fingerprint) @@ -388,7 +385,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.debug("I will sign key with fpr {}".format(fingerprint)) - keyring = Keyring() + keyring = GetNewKeyring() keyring.context.set_option('export-options', 'export-minimal') tmpkeyring = TempKeyringCopy(keyring) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 6ea7ab4..7d9fa57 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -55,9 +55,11 @@ def MinimalExport(keydata): return stripped_key -def GetKeyringCopy(): +def GetNewKeyring(): return Keyring() +def GetNewTempKeyring(): + return TempKeyring() class TempKeyringCopy(TempKeyring): """A temporary keyring which uses the secret keys of a parent keyring From 120591527236028a32cab32542fc7719b018e3c3 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 14:17:00 +0300 Subject: [PATCH 009/102] SignPages.py: Replaced monkeysign.gpg with keysign.gpg.gpg. We now import a GetNewKeyring function from gpg.py module. --- keysign/SignPages.py | 26 +++++----- keysign/gpg/gpg.py | 115 ------------------------------------------- 2 files changed, 12 insertions(+), 129 deletions(-) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index 757dcaa..0953730 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -23,7 +23,7 @@ import StringIO from gi.repository import GObject, Gtk, GLib, GdkPixbuf -from monkeysign.gpg import Keyring +from keysign.gpg.gpg import GetNewKeyring from qrencode import encode_scaled from datetime import datetime @@ -38,7 +38,7 @@ def parse_sig_list(text): '''Parses GnuPG's signature list (i.e. list-sigs) - + The format is described in the GnuPG man page''' sigslist = [] for block in text.split("\n"): @@ -55,10 +55,10 @@ def parse_sig_list(text): _keyring = None def signatures_for_keyid(keyid, keyring=None): '''Returns the list of signatures for a given key id - + This will call out to GnuPG list-sigs, using Monkeysign, and parse the resulting string into a list of signatures. - + A default Keyring will be used unless you pass an instance as keyring argument. ''' @@ -68,9 +68,7 @@ def signatures_for_keyid(keyid, keyring=None): if keyring is not None: kr = keyring else: - if _keyring is None: - _keyring = Keyring() - kr = _keyring + kr = _keyring if _keyring else GetNewKeyring() # FIXME: this would be better if it was done in monkeysign kr.context.call_command(['list-sigs', keyid]) @@ -113,7 +111,7 @@ def __init__(self, fpr=None): self.pack_start(leftVBox, True, True, 0) self.pack_start(self.rightVBox, True, True, 0) - + if self.fpr: self.setup_fingerprint_widget(self.fpr) @@ -161,7 +159,7 @@ def __init__(self): self.log = logging.getLogger() # FIXME: this should be moved to KeySignSection - self.keyring = Keyring() + self.keyring = GetNewKeyring() uidsLabel = Gtk.Label() uidsLabel.set_text("UIDs") @@ -211,8 +209,8 @@ def display_uids_signatures_page(self, openPgpKey): expiry = "No expiration date" self.expireLabel.set_markup(expiry) - - + + ### Set up signatures keyid = str(openPgpKey.keyid()) sigslist = signatures_for_keyid(keyid) @@ -230,10 +228,10 @@ def display_uids_signatures_page(self, openPgpKey): date = datetime.fromtimestamp(float(timestamp)) sigLabel.set_markup(str(keyid) + "\t\t" + date.ctime()) sigLabel.set_line_wrap(True) - + self.signaturesBox.pack_start(sigLabel, False, False, 0) sigLabel.show() - + sigLabel = Gtk.Label() sigLabel.set_markup("%d signatures" % len(sigslist)) sigLabel.set_line_wrap(True) @@ -299,7 +297,7 @@ def get_text_from_textview(self): start_iter = self.textbuffer.get_start_iter() end_iter = self.textbuffer.get_end_iter() raw_text = self.textbuffer.get_text(start_iter, end_iter, False) - + return raw_text diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7d9fa57..cdd24bb 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -116,121 +116,6 @@ def build_command(*args, **kwargs): - -def sign_key_async(keyring, data, fingerprint): - keyring.context.set_option('export-options', 'export-minimal') - - tmpkeyring = TempKeyringCopy(keyring) - keydata = data - - if keydata: - stripped_key = MinimalExport(keydata) - else: - log.debug("looking for key %s in your keyring", fingerprint) - keyring.context.set_option('export-options', 'export-minimal') - stripped_key = keyring.export_data(fingerprint) - - log.debug('Trying to import key\n%s', stripped_key) - - if tmpkeyring.import_data(stripped_key): - keys = tmpkeyring.get_keys(fingerprint) - log.info("Found keys %s for fp %s", keys, fingerprint) - assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) - key = keys[fingerprint] - uidlist = key.uidslist - - # FIXME: For now, we sign all UIDs. This is bad. - ret = tmpkeyring.sign_key(uidlist[0].uid, signall=True) - log.info("Result of signing %s on key %s: %s", uidlist[0].uid, fingerprint, ret) - - for uid in uidlist: - uid_str = uid.uid - log.info("Processing uid %s %s", uid, uid_str) - - # 3.2. export and encrypt the signature - # 3.3. mail the key to the user - signed_key = UIDExport(uid_str, tmpkeyring.export_data(uid_str)) - log.info("Exported %d bytes of signed key", len(signed_key)) - # self.signui.tmpkeyring.context.set_option('armor') - tmpkeyring.context.set_option('always-trust') - encrypted_key = tmpkeyring.encrypt_data(data=signed_key, recipient=uid_str) - - keyid = str(key.keyid()) - ctx = { - 'uid' : uid_str, - 'fingerprint': fingerprint, - 'keyid': keyid, - } - # We could try to dir=tmpkeyring.dir - # We do not use the with ... as construct as the - # tempfile might be deleted before the MUA had the chance - # to get hold of it. - # Hence we reference the tmpfile and hope that it will be properly - # cleaned up when this object will be destroyed... - tmpfile = NamedTemporaryFile(prefix='gnome-keysign-', suffix='.asc') - self.tmpfiles.append(tmpfile) - filename = tmpfile.name - log.info('Writing keydata to %s', filename) - tmpfile.write(encrypted_key) - # Interesting, sometimes it would not write the whole thing out, - # so we better flush here - tmpfile.flush() - # As we're done with the file, we close it. - #tmpfile.close() - - subject = Template(SUBJECT).safe_substitute(ctx) - body = Template(BODY).safe_substitute(ctx) - self.email_file (to=uid_str, subject=subject, - body=body, files=[filename]) - - -### From SignPages.py ### - -def parse_sig_list(text): - '''Parses GnuPG's signature list (i.e. list-sigs) - - The format is described in the GnuPG man page''' - sigslist = [] - for block in text.split("\n"): - if block.startswith("sig"): - record = block.split(":") - log.debug("sig record (%d) %s", len(record), record) - keyid, timestamp, uid = record[4], record[5], record[9] - sigslist.append((keyid, timestamp, uid)) - - return sigslist - - -# This is a cache for a keyring object, so that we do not need -# to create a new object every single time we parse signatures -_keyring = None -def signatures_for_keyid(keyid, keyring=None): - '''Returns the list of signatures for a given key id - - This will call out to GnuPG list-sigs, using Monkeysign, - and parse the resulting string into a list of signatures. - - A default Keyring will be used unless you pass an instance - as keyring argument. - ''' - # Retrieving a cached instance of a keyring, - # unless we were being passed a keyring - global _keyring - if keyring is not None: - kr = keyring - else: - if _keyring is None: - _keyring = Keyring() - kr = _keyring - - # FIXME: this would be better if it was done in monkeysign - kr.context.call_command(['list-sigs', keyid]) - siglist = parse_sig_list(kr.context.stdout) - - return siglist - - - ### Below are wrappers for gpg use cases that might be good to have in our ### implementation From dedd33413059dd4c55b385b31ac83f3274a296db Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 14:20:32 +0300 Subject: [PATCH 010/102] GPGQRCode.py: Replaced monkeysign.gpg with keysign.gpg.gpg import --- keysign/GPGQRCode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keysign/GPGQRCode.py b/keysign/GPGQRCode.py index 0a81b45..438fd54 100755 --- a/keysign/GPGQRCode.py +++ b/keysign/GPGQRCode.py @@ -20,20 +20,20 @@ for keys and selects the one matching your input """ from gi.repository import Gtk -from monkeysign.gpg import Keyring +from keysign.gpg.gpg import GetNewKeyring from QRCode import QRImage def main(): import sys key = sys.argv[1] - keyring = Keyring() + keyring = GetNewKeyring() keys = keyring.get_keys(key) # Heh, we take the first key here. Maybe we should raise a warning # or so, when there is more than one key. fpr = keys.items()[0][0] data = 'OPENPGP4FPR:' + fpr - + w = Gtk.Window() w.connect("delete-event", Gtk.main_quit) w.set_default_size(100,100) From 40f5933523f5aabaa94a116ed164be5c3361835c Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 14:31:18 +0300 Subject: [PATCH 011/102] Finished replacing all gpg calls from monkeysign.gpg with keysign.gpg.gpg. --- keysign/KeyPresent.py | 14 +++++++------- keysign/KeysPage.py | 36 ++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/keysign/KeyPresent.py b/keysign/KeyPresent.py index 1a1b3b2..d00ddbc 100644 --- a/keysign/KeyPresent.py +++ b/keysign/KeyPresent.py @@ -26,7 +26,7 @@ from gi.repository import Gtk, GLib from gi.repository import GObject -from monkeysign.gpg import Keyring +from keysign.gpg.gpg import GetNewKeyring # These are relative imports from __init__ import __version__ @@ -41,7 +41,7 @@ class KeyPresent(Gtk.Application): """A demo application showing how to display sufficient details about a key such that it can be sent securely. - + Note that the main purpose is to enable secure transfer, not reviewing key details. As such, the implementation might change a lot, depending on the method of secure transfer. @@ -127,10 +127,10 @@ def main(args=sys.argv): format='%(name)s (%(levelname)s): %(message)s') try: arguments = parse_command_line(args) - + #if arguments.gpg: # keyid = arguments.file - # keyring = Keyring() + # keyring = GetNewKeyring() # # this is a dict {fpr: key-instance} # found_keys = keyring.get_keys(keyid) # # We take the first item we found and export the actual keydata @@ -146,12 +146,12 @@ def main(args=sys.argv): GLib.unix_signal_add_full(GLib.PRIORITY_HIGH, signal.SIGINT, lambda *args : app.quit(), None) except AttributeError: pass - + exit_status = app.run(fpr) - + return exit_status - + finally: logging.shutdown() diff --git a/keysign/KeysPage.py b/keysign/KeysPage.py index 529d424..034f5bc 100644 --- a/keysign/KeysPage.py +++ b/keysign/KeysPage.py @@ -26,7 +26,7 @@ from gi.repository import Gtk, GLib from gi.repository import GObject -from monkeysign.gpg import Keyring +from keysign.gpg.gpg import GetNewKeyring # These are relative imports from __init__ import __version__ @@ -37,10 +37,10 @@ class KeysPage(Gtk.VBox): '''This represents a list of keys with the option for the user to select one key to proceed. - + This class emits a `key-selection-changed' signal when the user initially selects a key such that it is highlighted. - + The `key-selected' signal is emitted when the user commits to a key, i.e. by pressing a designated button to make his selection public. @@ -58,7 +58,7 @@ class KeysPage(Gtk.VBox): def __init__(self, show_public_keys=False): '''Sets the widget up. - + The show_public_keys parameter is meant for development purposes only. If set to True, the widget will show the public keys, too. Otherwise, secret keys are shown. @@ -72,7 +72,7 @@ def __init__(self, show_public_keys=False): self.store = Gtk.ListStore(str, str, str) # FIXME: this should be moved to KeySignSection - self.keyring = Keyring() # the user's keyring + self.keyring = GetNewKeyring() # the user's keyring self.keysDict = {} @@ -123,7 +123,7 @@ def __init__(self, show_public_keys=False): self.treeView.append_column(nameColumn) self.treeView.append_column(emailColumn) self.treeView.append_column(keyColumn) - + self.treeView.connect('row-activated', self.on_row_activated) # make the tree view resposive to single click selection @@ -171,12 +171,12 @@ def get_items_from_selection(self, selection=None): def on_selection_changed(self, selection, *args): log.debug('Selected new TreeView item %s = %s', selection, args) - + name, email, keyid = self.get_items_from_selection(selection) - + key = self.keysDict[keyid] self.emit('key-selection-changed', keyid) - + try: exp_date = datetime.fromtimestamp(float(key.expiry)) expiry = "{:%Y-%m-%d %H:%M:%S}".format(exp_date) @@ -205,7 +205,7 @@ def on_selection_changed(self, selection, *args): def on_row_activated(self, treeview, tree_path, column): '''A callback for when the user "activated" a row, e.g. by double-clicking an entry. - + It emits the key-selected signal. ''' # We just hijack the existing function. @@ -228,7 +228,7 @@ def on_publish_button_clicked(self, button, key, *args): class Keys(Gtk.Application): """A widget which displays keys in a user's Keyring. - + Once the user has selected a key, the key-selected signal will be thrown. """ @@ -271,7 +271,7 @@ def on_activate(self, app): def on_key_selection_changed(self, button, key): """This is the connected to the KeysPage's key-selection-changed signal - + As a user of that widget, you would show more details in the GUI or prepare for a final commitment by the user. """ @@ -280,13 +280,13 @@ def on_key_selection_changed(self, button, key): def on_key_selected(self, button, key): """This is the connected to the KeysPage's key-selected signal - + As a user of that widget, you would enable buttons or proceed with the GUI. """ self.log.info('User committed to a key! %s', key) - + def parse_command_line(argv): """Parse command line argument. See -h option @@ -319,18 +319,18 @@ def main(args=sys.argv): format='%(name)s (%(levelname)s): %(message)s') try: arguments = parse_command_line(args) - + app = Keys() try: GLib.unix_signal_add_full(GLib.PRIORITY_HIGH, signal.SIGINT, lambda *args : app.quit(), None) except AttributeError: pass - + exit_status = app.run(None) - + return exit_status - + finally: logging.shutdown() From dd74dd5553a3d0e273a0ff55eabff7cac16bc80d Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 25 Jun 2015 23:42:01 +0300 Subject: [PATCH 012/102] example.py: Created get_key and get_keylist functions. Added gpg test key2. Most of the functionality that we need for gpg interraction is demonstrated through the functions in example.py. Now all is left implement the actual needed functions that are in gpg.py module. --- keysign/gpg/example.py | 19 +++++++++++++++++-- keysign/gpg/gpg.py | 16 +++++++++------- keysign/gpg/keys/key2.pub | 20 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 keysign/gpg/keys/key2.pub diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py index e7060c5..944c444 100644 --- a/keysign/gpg/example.py +++ b/keysign/gpg/example.py @@ -16,8 +16,8 @@ log = logging.getLogger() keydir = os.path.join(os.path.dirname(__file__), 'keys') -test_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' - +test1_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' +test2_fpr = 'D09A409FC49466806CF79837946B842CDB8CFC4E' _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') @@ -53,6 +53,21 @@ def export_key(context, fpr, armor=True): return keydata +def get_key(context, fpr): + key = None + try: + key = context.get_key(fpr) + except gpgme.GpgmeError as err: + log.error('No key found with fpr %s', fpr) + + return key + + +def get_keylist(context, keyid = None, secret = False): + keys = [key for key in context.keylist(keyid, secret)] + return keys + + def main(): # set up the environment set_up_temp_dir() diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index cdd24bb..a96247a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -1,9 +1,5 @@ #!/usr/bin/env python -# FIXME: -# Extract all use cases where gpg support comes from monkeysign and -# add them to this file. - import logging from string import Template @@ -116,8 +112,8 @@ def build_command(*args, **kwargs): -### Below are wrappers for gpg use cases that might be good to have in our -### implementation +### Below functions represent gpg calls that mostly depend on +### monkeysign.gpg.Keyring and monkeysign.gpg.Context classes def keyring_set_option(keyring, option, value = None): try: @@ -159,6 +155,12 @@ def keyring_export_data(keyring, keyid): return keydata +def keyring_get_keys(keyring, keyid=None): + keys_dict = keyring.get_keys(keyid) + return keys_dict + + + if __name__ == '__main__': - keyring = GetKeyringCopy() + keyring = GetNewKeyring() print keyring.get_keys() \ No newline at end of file diff --git a/keysign/gpg/keys/key2.pub b/keysign/gpg/keys/key2.pub new file mode 100644 index 0000000..9d510e4 --- /dev/null +++ b/keysign/gpg/keys/key2.pub @@ -0,0 +1,20 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.14 (GNU/Linux) + +mI0EVYxdtQEEAJwXn/1s5EPES52H2rWwZIXS1rlf3o6y9bOTO6612Gfa83OZaIXF +00CIwZKGcFxHG4mV5Y4RPA67pz85ZRU1jt1a5D64cml16Qi/dOVoyRaBMtYiIWpn +oTlRS4f/3nf8yzDlaw/H+SPh7BzwGJTAJg2cP3ZdOMT6+6Ymi6pHahL/ABEBAAG0 +J0pvaG4gRG9lIChUZXN0IGtleSkgPGpvaG4uZG9lQHRlc3QuY29tPoi+BBMBAgAo +BQJVjF21AhsDBQkCMFubBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCUa4Qs +24z8Tv3WA/9P8GFg/Uuck6pXovxk/jr5vK36wW/+5BtlpN8MzqhEDk8z99gawbmQ +icYRx5gz1BjVQWx+kAXTJdlndMmOAXEMj2dzgh9Unom2b0eHLeRcXJQuc74cxCi/ +HLLrEOGK0RkSdqPrfJnzjT1RMGPRiqkSFuFO14kknvbJC9RjiebOX7iNBFWMXbUB +BADHUhCjBcTccShlKU/9Y7sO40BhvYfSV7UDXWzquTgvOK1ZWr5HaO47TNfKEPOw ++wTwnMwEn+MpLF2kvnSIUoG/i3kh6J3+5dIeRqk5M16GCABooP5H+ssDyVgLcoYa +cLdvyCf6wxjwgx5UbT+Y9KC27ruwiELHteZQ7+I+PNO6SQARAQABiKUEGAECAA8F +AlWMXbUCGwwFCQIwW5sACgkQlGuELNuM/E55EgQAmarSCXVt3Gsz+TWhMoB/2foj +mMheTVD5cp2vpoG24fIjwIN/q2ZGQb3roRtUOCyLmDyh6c8LOG1NFyVP8iVSR5uo +zZK8cpeptiGlUqM8WUTrxzrxhsd57oqKPS2UQZLhN4YcAvoYAIAArh4Bc+WijY0n +1jywSvDM7oCdjEKy4OM= +=jzft +-----END PGP PUBLIC KEY BLOCK----- From 37ce77406eb2640705874a0d718b3df543019e3e Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 26 Jun 2015 23:30:22 +0300 Subject: [PATCH 013/102] gpg.py: Added a gpgme implementation of UIDExport (not yet functional). We are calling UIDExport from GetKeySection.sign_key_async method and we do that for every UID of a key that we want to sign. Therefore , for each UID, the call to UIDExport does the following: * imports the key into a temp keyring * removes all other UIDs except the current one passed * returns the exported keydata with the UIDs removed This is how we sign all UIDs and email back the key with its corresponding UID. Unfortunately PyGPGME API doesn't seem to offer an easy way to delete an UID (or I don't know yet) so for know we don't use the UIDExport_gpgme implementation. --- keysign/Sections.py | 1 + keysign/gpg/gpg.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/keysign/Sections.py b/keysign/Sections.py index f8006fd..a231244 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -31,6 +31,7 @@ from monkeysign.ui import MonkeysignUi from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, GetNewTempKeyring, TempKeyringCopy +from keysign.gpg.gpg import UIDExport_gpgme from compat import gtkbutton import Keyserver diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a96247a..10a4a3a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -3,14 +3,72 @@ import logging from string import Template +import os import shutil +import tempfile from tempfile import NamedTemporaryFile from monkeysign.gpg import Keyring, TempKeyring +import gpgme +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO log = logging.getLogger() + +_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') +def set_up_temp_dir(): + os.environ['GNUPGHOME'] = _gpghome + # Copy secrets from .gnupg to temporary dir + try: + from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' + to_ = _gpghome + shutil.copy(from_, to_) + log.debug('copied your gpg.conf from %s to %s', from_, to_) + except IOError as e: + log.error('User has no gpg.conf file') + +def remove_temp_dir(): + del os.environ['GNUPGHOME'] + shutil.rmtree(_gpghome, ignore_errors=True) + + + +def UIDExport_gpgme(uid, keydata): + set_up_temp_dir() + + ctx = gpgme.Context() + # XXX: do we need to set a "always-trust" flag ? + data = BytesIO(keydata) + result = ctx.import_(data) + # check if import key had succes; no error is thrown by gpgme if wrong keydata + assert result.considered == 1, "Key for uid %s was not correctly imported" % (uid,) + assert result.imported == 1, "Key for uid %s was not correctly imported" % (uid,) + assert len(result.imports) == 1, "Key for uid %s was not correctly imported" % (uid,) + + keys = [key for key in ctx.keylist(uid)] + for key in keys: + for u in key.uids: + # XXX: at this moment I don't know a way to delete other UIDs from the key + # so in the end we have only the uid that must be signed + if u != uid: + log.info('Deleting UID %s from key %s', u.uid, key.subkeys[0].fpr) + try: + key.uids.remove(u) + except ValueError as err: + log.error("Couldn't delete UID %s from key %s", u.uid, key.subkeys[0].fpr) + + keydata = BytesIO() + ctx.export(uid, keydata) + + remove_temp_dir() + return keydata.getvalue() + + + ### From Sections.py ### def UIDExport(uid, keydata): From de8404763d38e007263acc1ba311eda2d5c28954 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 26 Jun 2015 23:50:57 +0300 Subject: [PATCH 014/102] gpg.py: Surrounded key import with try-catch block. It seems that gpgme.Context.import_ call can throw a gpgm.GpgmeError when the data to import is not keydata. --- keysign/gpg/gpg.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 10a4a3a..06e5a34 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -43,8 +43,12 @@ def UIDExport_gpgme(uid, keydata): ctx = gpgme.Context() # XXX: do we need to set a "always-trust" flag ? data = BytesIO(keydata) - result = ctx.import_(data) - # check if import key had succes; no error is thrown by gpgme if wrong keydata + try: + result = ctx.import_(data) + except gpgme.GpgmeError as err: + log.error("Couldn't import the key with the following keydata:\n%s", keydata) + return None + assert result.considered == 1, "Key for uid %s was not correctly imported" % (uid,) assert result.imported == 1, "Key for uid %s was not correctly imported" % (uid,) assert len(result.imports) == 1, "Key for uid %s was not correctly imported" % (uid,) From 7785aa8c3653357a715555bd5d213cae3a9765c0 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 1 Jul 2015 23:40:40 +0300 Subject: [PATCH 015/102] Made it to raise ValueError instead of returning None value. The gpg API can change again in the future depending on how we'll replace the current gpg wrapper. Removed unnecesary asserts from UIDExport_gpgm. --- keysign/gpg/example.py | 1 + keysign/gpg/gpg.py | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py index 944c444..5ba0958 100644 --- a/keysign/gpg/example.py +++ b/keysign/gpg/example.py @@ -59,6 +59,7 @@ def get_key(context, fpr): key = context.get_key(fpr) except gpgme.GpgmeError as err: log.error('No key found with fpr %s', fpr) + raise ValueError('Invalid fingerprint') return key diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 06e5a34..f2f6441 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -47,11 +47,7 @@ def UIDExport_gpgme(uid, keydata): result = ctx.import_(data) except gpgme.GpgmeError as err: log.error("Couldn't import the key with the following keydata:\n%s", keydata) - return None - - assert result.considered == 1, "Key for uid %s was not correctly imported" % (uid,) - assert result.imported == 1, "Key for uid %s was not correctly imported" % (uid,) - assert len(result.imports) == 1, "Key for uid %s was not correctly imported" % (uid,) + raise ValueError('Invalid keydata') keys = [key for key in ctx.keylist(uid)] for key in keys: From 19ec326a2bff4e227c6a81315b2bf66718f3f061 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 3 Jul 2015 00:06:18 +0300 Subject: [PATCH 016/102] Added a KeyringGPG class. This replaces the keyring functionalities offered by Keyring class. --- keysign/gpg/example.py | 4 +-- keysign/gpg/gpg.py | 71 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py index 5ba0958..7301a11 100644 --- a/keysign/gpg/example.py +++ b/keysign/gpg/example.py @@ -50,7 +50,7 @@ def export_key(context, fpr, armor=True): context.armor = armor keydata = BytesIO() context.export(fpr, keydata) - return keydata + return keydata.getvalue() def get_key(context, fpr): @@ -64,7 +64,7 @@ def get_key(context, fpr): return key -def get_keylist(context, keyid = None, secret = False): +def get_keys_iterator(context, keyid = None, secret = False): keys = [key for key in context.keylist(keyid, secret)] return keys diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index f2f6441..84ebc20 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -67,6 +67,77 @@ def UIDExport_gpgme(uid, keydata): remove_temp_dir() return keydata.getvalue() +class KeyringGPG: + """A class that has the functionalities of a keyring + + It aggregates a gpgme.Context object through which executes + gpg calls because normal inheritance isn't possible. + """ + + def __init__(self, tmphome = True): + self.log = logging.getLogger() + self._gpghome = None + + if tmphome: + self._gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') + self.set_up_tmp_dir() + + self.ctx = gpgme.Context() + + + def __del__(self): + if self._gpghome: + del os.environ['GNUPGHOME'] + shutil.rmtree(self._gpghome, ignore_errors=True) + + + def set_up_tmp_dir(self): + os.environ['GNUPGHOME'] = self._gpghome + # Copy secrets from .gnupg to temporary dir + try: + from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' + to_ = self._gpghome + shutil.copy(from_, to_) + self.log.debug('copied your gpg.conf from %s to %s', from_, to_) + except IOError as e: + self.log.error('User has no gpg.conf file') + + + def import_data(self, keydata): + data = BytesIO(keydata) + try: + result = self.ctx.import_(data) + except gpgme.GpgmeError as err: + self.log.error("Couldn't import the key with the following keydata:\n%s", keydata) + return False + # XXX: we stick to return True/False for compatibility issues. + # The gpgme.ImportResult can be used to extract more information. + return True + + + def export_data(self, fpr, armor = True): + self.ctx.armor = armor + keydata = BytesIO() + self.ctx.export(fpr, keydata) + + return keydata.getvalue() + + + def get_key(self, fpr): + key = None + try: + key = self.ctx.get_key(fpr) + except gpgme.GpgmeError as err: + self.log.error('No key found with fpr %s', fpr) + raise ValueError('Invalid fingerprint') + + return key + + + def get_keys_iterator(self, keyid = None, secret = False): + keys = [key for key in context.keylist(keyid, secret)] + return keys + ### From Sections.py ### From 0999eaed612f8f1e760b9d688361f5e6bbb7c4ab Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 5 Jul 2015 14:32:12 +0300 Subject: [PATCH 017/102] Renamed method to resemble better what it returns. --- keysign/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 84ebc20..725d293 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -134,7 +134,7 @@ def get_key(self, fpr): return key - def get_keys_iterator(self, keyid = None, secret = False): + def get_keylist(self, keyid = None, secret = False): keys = [key for key in context.keylist(keyid, secret)] return keys From fb9f5296dd782ec35c4e95d94e8f3f4ad2bacdf5 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 13:18:16 +0300 Subject: [PATCH 018/102] on_key_selected: replaces the gpg calls with gpgme Most of the functionalities of a keyring is done in gpgme.Context class in PyGPGME. This is good because it removes a layer of abstraction. --- keysign/Sections.py | 19 +++++++++++++------ keysign/SignPages.py | 6 +++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index a231244..64e4ae4 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -49,6 +49,11 @@ # Needed for window.get_xid(), xvimagesink.set_window_handle(), respectively: from gi.repository import GstVideo +import gpgme +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO Gst.init([]) @@ -91,7 +96,7 @@ def __init__(self, app): self.app = app self.log = logging.getLogger() - self.keyring = GetNewKeyring() + self.ctx = gpgme.Context() # these are needed later when we need to get details about # a selected key @@ -145,12 +150,14 @@ def on_key_selected(self, pane, keyid): ''' self.log.info('User selected key %s', keyid) - key = self.keyring.get_keys(keyid).values()[0] + key = self.ctx.get_key(keyid) + fpr = key.subkeys[0].fpr - keyid = key.keyid() - fpr = key.fpr - self.keyring.export_data(fpr, secret=False) - keydata = self.keyring.context.stdout + export_data = BytesIO() + self.ctx.armor = True + + self.ctx.export(fpr, export_data) + keydata = export_data.getvalue() self.log.debug("Keyserver switched on! Serving key with fpr: %s", fpr) self.app.setup_server(keydata, fpr) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index 0953730..b33e3b5 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -116,10 +116,10 @@ def __init__(self, fpr=None): self.setup_fingerprint_widget(self.fpr) - def display_fingerprint_qr_page(self, openPgpKey=None): - assert openPgpKey or self.fpr + def display_fingerprint_qr_page(self, gpgmeKey=None): + assert gpgmeKey or self.fpr - rawfpr = openPgpKey.fpr if openPgpKey else self.fpr + rawfpr = gpgmeKey.subkeys[0].fpr if gpgmeKey else self.fpr self.fpr = rawfpr self.setup_fingerprint_widget(self.fpr) From c37bfdddabecae43cf9c843a7cac0e83b1ae2c48 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 14:21:30 +0300 Subject: [PATCH 019/102] gpg: Added a default homedir field to KeyringGPG --- keysign/gpg/gpg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 725d293..146acbe 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -74,6 +74,8 @@ class KeyringGPG: gpg calls because normal inheritance isn't possible. """ + default_gpghome = os.environ['HOME'] + '/.gnupg/' + def __init__(self, tmphome = True): self.log = logging.getLogger() self._gpghome = None From adb0c54cf537fc03f6bdc7653e6d5f27c5b5b731 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 14:29:21 +0300 Subject: [PATCH 020/102] KeyringGPG: check if gpg home is temporary before removing dir --- keysign/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 146acbe..bae6d39 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -88,7 +88,7 @@ def __init__(self, tmphome = True): def __del__(self): - if self._gpghome: + if self._gpghome and not self._gpghome.startswith(default_gpghome): del os.environ['GNUPGHOME'] shutil.rmtree(self._gpghome, ignore_errors=True) From d3330496a01b4f598e4c8476fb3dab88a14f037e Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 14:31:18 +0300 Subject: [PATCH 021/102] KeyringGPG: updated docstrings --- keysign/gpg/gpg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index bae6d39..0119dbe 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -70,8 +70,8 @@ def UIDExport_gpgme(uid, keydata): class KeyringGPG: """A class that has the functionalities of a keyring - It aggregates a gpgme.Context object through which executes - gpg calls because normal inheritance isn't possible. + It aggregates a gpgme.Context object through which executes gpg calls. + Aggregation is used because normal inheritance isn't possible. """ default_gpghome = os.environ['HOME'] + '/.gnupg/' From 9aa3d9969e7dcf94af90a8fa7c726c12c2d14ec9 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 17:20:04 +0300 Subject: [PATCH 022/102] set_up_tmp_dir: moved the secrets copying to another function --- keysign/gpg/gpg.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 0119dbe..40ddae4 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -22,6 +22,14 @@ _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') def set_up_temp_dir(): os.environ['GNUPGHOME'] = _gpghome + + +def remove_temp_dir(): + del os.environ['GNUPGHOME'] + shutil.rmtree(_gpghome, ignore_errors=True) + + +def copy_secrets(): # Copy secrets from .gnupg to temporary dir try: from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' @@ -31,11 +39,6 @@ def set_up_temp_dir(): except IOError as e: log.error('User has no gpg.conf file') -def remove_temp_dir(): - del os.environ['GNUPGHOME'] - shutil.rmtree(_gpghome, ignore_errors=True) - - def UIDExport_gpgme(uid, keydata): set_up_temp_dir() From 075982170d143e5876139aecbc844da0c2169460 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 18:09:11 +0300 Subject: [PATCH 023/102] gpg: Added function for extracting fingerpring and key data of a key. It does that by passing a context (gpgme.Context) which should contain the desired key. --- keysign/gpg/gpg.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 40ddae4..92fc09d 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -40,6 +40,25 @@ def copy_secrets(): log.error('User has no gpg.conf file') +def extract_fpr(gpgmeContext, keyid): + # Extracts the fingerprint from a key with id: keyid + try: + key = gpgmeContext.get_key(keyid) + except gpgme.GpgmeError as err: + log.error('No key found with id: %s', keyid) + raise ValueError("Invalid keyid") + + # A gpgme.Key object doesn't have a fpr but a gpgme.Subkey does + return key.subkeys[0].fpr + + +def extract_keydata(gpgmeContext, fpr, armor=False): + gpgmeContext.armor = armor + keydata = BytesIO() + gpgmeContext.export(fpr, keydata) + return keydata.getvalue() + + def UIDExport_gpgme(uid, keydata): set_up_temp_dir() From d065d8f9933d85f087e06ac13b5bf19fe5f71881 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 19:25:14 +0300 Subject: [PATCH 024/102] on_key_selected: made it call gpg functions from gpg library. --- keysign/Sections.py | 16 ++++++---------- keysign/SignPages.py | 6 +++--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 64e4ae4..cc41514 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -30,6 +30,7 @@ import sys from monkeysign.ui import MonkeysignUi +from keysign.gpg import gpg from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, GetNewTempKeyring, TempKeyringCopy from keysign.gpg.gpg import UIDExport_gpgme @@ -150,27 +151,22 @@ def on_key_selected(self, pane, keyid): ''' self.log.info('User selected key %s', keyid) - key = self.ctx.get_key(keyid) - fpr = key.subkeys[0].fpr + fpr = gpg.extract_fpr(self.ctx, keyid) - export_data = BytesIO() - self.ctx.armor = True - - self.ctx.export(fpr, export_data) - keydata = export_data.getvalue() + keydata = gpg.extract_keydata(self.ctx, fpr, True) self.log.debug("Keyserver switched on! Serving key with fpr: %s", fpr) self.app.setup_server(keydata, fpr) - self.switch_to_key_present_page(key) + self.switch_to_key_present_page(fpr) - def switch_to_key_present_page(self, key): + def switch_to_key_present_page(self, fpr): '''This switches the notebook to the page which presents the information that is needed to securely transfer the keydata, i.e. the fingerprint and its barcode. ''' - self.keyPresentPage.display_fingerprint_qr_page(key) + self.keyPresentPage.display_fingerprint_qr_page(fpr) self.notebook.next_page() # This is more of a crude hack. Once the next page is presented, # the back button has the focus. This is not desirable because diff --git a/keysign/SignPages.py b/keysign/SignPages.py index b33e3b5..41eeaf4 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -116,10 +116,10 @@ def __init__(self, fpr=None): self.setup_fingerprint_widget(self.fpr) - def display_fingerprint_qr_page(self, gpgmeKey=None): - assert gpgmeKey or self.fpr + def display_fingerprint_qr_page(self, fpr): + assert fpr or self.fpr - rawfpr = gpgmeKey.subkeys[0].fpr if gpgmeKey else self.fpr + rawfpr = fpr if fpr else self.fpr self.fpr = rawfpr self.setup_fingerprint_widget(self.fpr) From 8493c9297aa5add0bba0aa048e5c8eeee77b7e53 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 19:54:12 +0300 Subject: [PATCH 025/102] gpg: Updated docstrings --- keysign/gpg/gpg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 92fc09d..3eb9d51 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -41,7 +41,8 @@ def copy_secrets(): def extract_fpr(gpgmeContext, keyid): - # Extracts the fingerprint from a key with id: keyid + """Extracts the fingerprint of a key with @keyid. + """ try: key = gpgmeContext.get_key(keyid) except gpgme.GpgmeError as err: @@ -53,6 +54,9 @@ def extract_fpr(gpgmeContext, keyid): def extract_keydata(gpgmeContext, fpr, armor=False): + """Extracts key data from a key with fingerprint @fpr. + Returns the data in plaintext (if armor=True) or binary. + """ gpgmeContext.armor = armor keydata = BytesIO() gpgmeContext.export(fpr, keydata) From f5fbdf0c57c45a7b2f1303862a526e30c8ca1d93 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 20:02:43 +0300 Subject: [PATCH 026/102] gpg: More docstrings update --- keysign/gpg/gpg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 3eb9d51..cfba9f2 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -21,10 +21,14 @@ _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') def set_up_temp_dir(): + """Sets up a temporary directory as gnupg home + """ os.environ['GNUPGHOME'] = _gpghome def remove_temp_dir(): + """Removes the directory for gnugp home + """ del os.environ['GNUPGHOME'] shutil.rmtree(_gpghome, ignore_errors=True) From 5e2530f134e48a133d0af7976800940db3b42c77 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jul 2015 20:05:14 +0300 Subject: [PATCH 027/102] gpg: enclosed deletion of system environment var in try-catch block --- keysign/gpg/gpg.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index cfba9f2..6066d2f 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -29,7 +29,11 @@ def set_up_temp_dir(): def remove_temp_dir(): """Removes the directory for gnugp home """ - del os.environ['GNUPGHOME'] + try: + del os.environ['GNUPGHOME'] + except KeyError as err: + log.error("'GNUPGHOME key not set.'") + return shutil.rmtree(_gpghome, ignore_errors=True) From c2a9f26fad12a2a2cf67ffa240bba15dab9647d2 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 8 Jul 2015 18:34:30 +0300 Subject: [PATCH 028/102] gpg: made copy_secrets to copy the secret keys now. It does that by calling import_key_to_tmp for each secret key found for user. This needs more testing because because gpgme doesn't have a way to set additional argument '--homedir' but instead relies to where 'GNUPGHOME' points to. --- keysign/gpg/gpg.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 6066d2f..e6f99a2 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -18,8 +18,9 @@ log = logging.getLogger() - +orig_gpghome = os.environ['HOME'] + '/.gnupg/' _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') + def set_up_temp_dir(): """Sets up a temporary directory as gnupg home """ @@ -37,16 +38,45 @@ def remove_temp_dir(): shutil.rmtree(_gpghome, ignore_errors=True) -def copy_secrets(): - # Copy secrets from .gnupg to temporary dir +def copy_secrets(gpgmeContext): + """Copies secrets from .gnupg to temporary dir + """ try: - from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' + from_ = orig_gpghome + 'gpg.conf' to_ = _gpghome shutil.copy(from_, to_) log.debug('copied your gpg.conf from %s to %s', from_, to_) except IOError as e: log.error('User has no gpg.conf file') + # Copy the public parts of the secret keys to the tmpkeyring + secret_keys = [key for key in gpgmeContext.keylist(None, True)] + + gpgtemp = _gpghome + for key in secret_keys: + if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: + import_key_to_tmp(gpgmeContext, key, gpgtemp) + + +def import_key_to_tmp(gpgmeContext, gpgmeKey, gpghome): + """Imports a key into a temporary keyring. + + Note that in gpgme, after you change the default gpg homedir, all + new and old context objects will use that new dir as their gpg homedir. + """ + keydata = extract_keydata(gpgmeContext, gpgmeKey.subkeys[0].fpr) + + os.environ['GNUPGHOME'] = gpghome + ctx = gpgme.Context() + # GPGME works with file descriptors, not the actual text of data + fp = BytesIO(keydata) + res = ctx.import_(fp) + + # Reset gpg home to original + os.environ['GNUPGHOME'] = orig_gpghome + return res + + def extract_fpr(gpgmeContext, keyid): """Extracts the fingerprint of a key with @keyid. From e82809a235a64c746cb88969ea07fb1bc7273be8 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 11 Jul 2015 01:27:32 +0300 Subject: [PATCH 029/102] gpg: Made import_key_to_tmpdir to import a key without changing globally gpg home It now changes the curent context' home by calling set_engine_info() with 3 arguments: * protocol (in our case PROTOCOL_OpenPGP) * abs path for executable that use the above protocol * dir name which will be used as home --- keysign/gpg/gpg.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index e6f99a2..e4ece9a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -7,13 +7,12 @@ import shutil import tempfile from tempfile import NamedTemporaryFile +from distutils.spawn import find_executable from monkeysign.gpg import Keyring, TempKeyring import gpgme -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO +from io import BytesIO +from StringIO import StringIO log = logging.getLogger() @@ -55,27 +54,28 @@ def copy_secrets(gpgmeContext): gpgtemp = _gpghome for key in secret_keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: - import_key_to_tmp(gpgmeContext, key, gpgtemp) + import_key_to_tmpdir(gpgmeContext, key.subkeys[0].fpr, gpgtemp) -def import_key_to_tmp(gpgmeContext, gpgmeKey, gpghome): +def import_key_to_tmpdir(gpgmeContext, fpr, gpghome): """Imports a key into a temporary keyring. - Note that in gpgme, after you change the default gpg homedir, all - new and old context objects will use that new dir as their gpg homedir. + It uses Context.set_engine_info() to restrict the change of gpg + directory to current context, elsewhere it would change it globally + through os.environ['GNUPGHOME'] """ - keydata = extract_keydata(gpgmeContext, gpgmeKey.subkeys[0].fpr) + gpg_path = find_executable('gpg') - os.environ['GNUPGHOME'] = gpghome - ctx = gpgme.Context() - # GPGME works with file descriptors, not the actual text of data - fp = BytesIO(keydata) - res = ctx.import_(fp) + tmp_ctx = gpgme.Context() + tmp_ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpghome) + + keydata = extract_keydata(gpgmeContext, fpr, True) - # Reset gpg home to original - os.environ['GNUPGHOME'] = orig_gpghome - return res + # It seems that keys can be imported through string streams only + keydataIO = StringIO(keydata) + res = ctx.import_(keydataIO) + return len(res.imports) != 0 def extract_fpr(gpgmeContext, keyid): From a7424737c8acfb2cec59105de25583c189294c8d Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 11 Jul 2015 01:51:54 +0300 Subject: [PATCH 030/102] gpg: Fixed import_key_to_tmpdir --- keysign/gpg/gpg.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index e4ece9a..bc87ec0 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -57,7 +57,7 @@ def copy_secrets(gpgmeContext): import_key_to_tmpdir(gpgmeContext, key.subkeys[0].fpr, gpgtemp) -def import_key_to_tmpdir(gpgmeContext, fpr, gpghome): +def import_key_to_tmpdir(gpgmeContext, fpr, new_homedir): """Imports a key into a temporary keyring. It uses Context.set_engine_info() to restrict the change of gpg @@ -66,14 +66,14 @@ def import_key_to_tmpdir(gpgmeContext, fpr, gpghome): """ gpg_path = find_executable('gpg') - tmp_ctx = gpgme.Context() - tmp_ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpghome) - - keydata = extract_keydata(gpgmeContext, fpr, True) - - # It seems that keys can be imported through string streams only + # The default context has access to user's default keyring + ctx = gpgme.Context() + keydata = extract_keydata(ctx, fpr, True) + # It seems that keys can be imported from string streams only keydataIO = StringIO(keydata) - res = ctx.import_(keydataIO) + + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpghome) + res = gpgmeContext.import_(keydataIO) return len(res.imports) != 0 From b8784f52d1d305a5c23d53fa58e7c48ddf355d07 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 13 Jul 2015 13:56:00 +0300 Subject: [PATCH 031/102] Renamed gpg library function and made gpghome optional argument --- keysign/gpg/gpg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index bc87ec0..72e2cdf 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -54,16 +54,17 @@ def copy_secrets(gpgmeContext): gpgtemp = _gpghome for key in secret_keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: - import_key_to_tmpdir(gpgmeContext, key.subkeys[0].fpr, gpgtemp) + import_key_by_fpr(gpgmeContext, key.subkeys[0].fpr, gpgtemp) -def import_key_to_tmpdir(gpgmeContext, fpr, new_homedir): +def import_key_by_fpr(gpgmeContext, fpr, new_homedir=None): """Imports a key into a temporary keyring. It uses Context.set_engine_info() to restrict the change of gpg directory to current context, elsewhere it would change it globally through os.environ['GNUPGHOME'] """ + new_homedir = new_homedir if new_homedir else tempfile.mkdtemp(prefix='tmp.gpghome') gpg_path = find_executable('gpg') # The default context has access to user's default keyring @@ -72,7 +73,7 @@ def import_key_to_tmpdir(gpgmeContext, fpr, new_homedir): # It seems that keys can be imported from string streams only keydataIO = StringIO(keydata) - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpghome) + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, new_homedir) res = gpgmeContext.import_(keydataIO) return len(res.imports) != 0 From 12479979671e34bc2ff304739d8b3eb2173903a3 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 13 Jul 2015 14:36:46 +0300 Subject: [PATCH 032/102] gpg: set_up_temp_dir sets now gnupg homedir only for current context --- keysign/gpg/gpg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 72e2cdf..0f048b8 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -20,10 +20,13 @@ orig_gpghome = os.environ['HOME'] + '/.gnupg/' _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') -def set_up_temp_dir(): - """Sets up a temporary directory as gnupg home + +def set_up_temp_dir(gpgmeContext, temp_dir=tempfile.mkdtemp(prefix='tmp.gpghome')): + """Sets up a temporary directory as new gnupg home + for this context """ - os.environ['GNUPGHOME'] = _gpghome + gpg_path = find_executable('gpg') + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, temp_dir) def remove_temp_dir(): From 7da0c7bd877bed815ea31e58f916340fff7d40df Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 13 Jul 2015 23:07:44 +0300 Subject: [PATCH 033/102] gpg: removed lib function and made it to reset gnupg dir to its default location --- keysign/gpg/gpg.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 0f048b8..b5f2280 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -29,15 +29,14 @@ def set_up_temp_dir(gpgmeContext, temp_dir=tempfile.mkdtemp(prefix='tmp.gpghome' gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, temp_dir) -def remove_temp_dir(): - """Removes the directory for gnugp home +def reset_gpg_dir(gpgmeContext): + """Resets the gnupg dir to its default location + for current context """ - try: - del os.environ['GNUPGHOME'] - except KeyError as err: - log.error("'GNUPGHOME key not set.'") - return - shutil.rmtree(_gpghome, ignore_errors=True) + default_gpghome = os.environ['HOME'] + '/.gnupg/' + gpg_path = find_executable('gpg') + + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, default_gpghome) def copy_secrets(gpgmeContext): From 8c2b3d0c7251093fcd3840327bebbb928c419cd2 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 13 Jul 2015 23:43:15 +0300 Subject: [PATCH 034/102] gpg: Renamed library functions for setting and reseting temp gnupg dir --- keysign/gpg/gpg.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index b5f2280..7531ab1 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -21,15 +21,18 @@ _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') -def set_up_temp_dir(gpgmeContext, temp_dir=tempfile.mkdtemp(prefix='tmp.gpghome')): +def gpg_set_dir(gpgmeContext, dir_prefix=None): """Sets up a temporary directory as new gnupg home for this context """ + dir_prefix = dir_prefix if dir_prefix else 'tmp.gpghome' + temp_dir = tempfile.mkdtemp(prefix=dir_prefix) + gpg_path = find_executable('gpg') gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, temp_dir) -def reset_gpg_dir(gpgmeContext): +def gpg_reset_dir(gpgmeContext): """Resets the gnupg dir to its default location for current context """ From 631c4c3ff228d61e575888f2afc41890018f91d1 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 14 Jul 2015 11:20:55 +0300 Subject: [PATCH 035/102] Sections.py: Prepared GetKeySection for replacing gpg calls with new gpg library --- keysign/Sections.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keysign/Sections.py b/keysign/Sections.py index cc41514..1185764 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -217,6 +217,7 @@ def __init__(self, app): # the temporary keyring we operate in self.tmpkeyring = None + self.ctx = None self.scanPage = ScanFingerprintPage() self.signPage = SignKeyPage() @@ -352,6 +353,10 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) #FIXME: should we create a new TempKeyring for each key we want # to sign it ? self.tmpkeyring = GetNewTempKeyring() + # For each key downloaded we create a new gpgme.Context object and + # set up a temporary dir for gpg + self.ctx = gpgme.Context() + gpg.gpg_set_dir(self.ctx) other_clients = self.sort_clients(other_clients, fingerprint) From 822939df1ea5cb89a27debb76602bc133b5cc404 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 14 Jul 2015 11:38:17 +0300 Subject: [PATCH 036/102] gpg: Added gpg_import_keydata lib function which imports a key from its keydata --- keysign/gpg/gpg.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7531ab1..d783286 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -84,6 +84,22 @@ def import_key_by_fpr(gpgmeContext, fpr, new_homedir=None): return len(res.imports) != 0 +def gpg_import_keydata(gpgmeContext, keydata): + """Tries to import a OpenPGP key from @keydata + + The @gpgmeContext object has a gpg directory already set. + """ + keydataIO = StringIO(keydata) + try: + result = gpgmeContext.import_(keydataIO) + except gpgme.GpgmeError as err: + log.error("Couldn't import the key with the following keydata:\n%s", keydataIO.getvalue()) + return False + # XXX: we stick to return True/False for compatibility issues. + # The gpgme.ImportResult can be used to extract more information. + return True + + def extract_fpr(gpgmeContext, keyid): """Extracts the fingerprint of a key with @keyid. """ From 4340dd15eabcd346db95df67ecf40148bf15340b Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 15 Jul 2015 11:55:59 +0300 Subject: [PATCH 037/102] Sections.py: Added calls from gpg library along with old monkeysign API --- keysign/Sections.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 1185764..ebc3b87 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -326,7 +326,10 @@ def try_download_keys(self, clients): def verify_downloaded_key(self, downloaded_data, fingerprint): # FIXME: implement a better and more secure way to verify the key - if self.tmpkeyring.import_data(downloaded_data): + # FIXME2: We keep the calls to old API until we replaced it entirely + self.tmpkeyring.import_data(downloaded_data) + + if gpg.gpg_import_keydata(self.ctx, downloaded_data): imported_key_fpr = self.tmpkeyring.get_keys().keys()[0] if imported_key_fpr == fingerprint: result = True From 8c90fac29ed08a23db31957489752f32b5a35147 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 15 Jul 2015 12:02:11 +0300 Subject: [PATCH 038/102] gpg: Encoded keydata as 'utf-8' because unicode doesn't work with PyGPGME import --- keysign/gpg/gpg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index d783286..7c9930a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -89,6 +89,9 @@ def gpg_import_keydata(gpgmeContext, keydata): The @gpgmeContext object has a gpg directory already set. """ + # XXX: PyGPGME key imports doesn't work with data as unicode strings + # but here we get data coming from network which is unicode + keydata = keydata.encode('utf-8') keydataIO = StringIO(keydata) try: result = gpgmeContext.import_(keydataIO) From e8a159c30620256c1ce955bc7d404493f55dfb0c Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 15 Jul 2015 13:17:43 +0300 Subject: [PATCH 039/102] Added gpg lib function and replaced more old API calls in Sections.py --- keysign/Sections.py | 2 +- keysign/gpg/gpg.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index ebc3b87..546345a 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -330,7 +330,7 @@ def verify_downloaded_key(self, downloaded_data, fingerprint): self.tmpkeyring.import_data(downloaded_data) if gpg.gpg_import_keydata(self.ctx, downloaded_data): - imported_key_fpr = self.tmpkeyring.get_keys().keys()[0] + imported_key_fpr = gpg.gpg_get_keylist(self.ctx, None, False)[0].subkeys[0].fpr if imported_key_fpr == fingerprint: result = True else: diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7c9930a..a26bcb5 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -103,6 +103,15 @@ def gpg_import_keydata(gpgmeContext, keydata): return True +def gpg_get_keylist(gpgmeContext, keyid=None, secret=False): + """Returns the keys found in @gpgmeContext + If @keyid is None then all geys will be returned. + If @secret=True then it will return the secret keys. + """ + keys = [key for key in gpgmeContext.keylist(keyid, secret)] + return keys + + def extract_fpr(gpgmeContext, keyid): """Extracts the fingerprint of a key with @keyid. """ From ee45d05b7651359e932b0b2e93cd2aa03be6d223 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 15 Jul 2015 15:52:48 +0300 Subject: [PATCH 040/102] gpg: Added two functions to help with displaying info about a key. --- keysign/gpg/gpg.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a26bcb5..00b7a0c 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -135,6 +135,47 @@ def extract_keydata(gpgmeContext, fpr, armor=False): return keydata.getvalue() +def format_fpr(fpr): + """display a clean version of the fingerprint + + this is the display we usually see + """ + l = list(fpr) # explode + s = '' + for i in range(10): + # output 4 chars + s += ''.join(l[4*i:4*i+4]) + # add a space, except at the end + if i < 9: s += ' ' + # add an extra space in the middle + if i == 4: s += ' ' + return s + + +def gpg_format_key(gpgmeKey): + """Returns a string representation of @gpgmeKey + + It contains info about: length of key, keyid, expiration date, creation date, + fingerprint, uids and subkeys. + """ + subkey = gpgmeKey.subkeys[0] + + ret = u'pub %sR/%s' % (subkey.length, subkey.fpr[-8:]) + ret += u' %s' % (subkey.timestamp, ) + if subkey.expires: ret += u' [expires: %s]' % (subkey.expires,) + ret += '\n' + ret += u' Fingerprint = %s\n' % (format_fpr(subkey.fpr),) + i = 1 + for uid in gpgmeKey.uids: + ret += u"uid %d %s\n" % (i, uid.uid.decode('utf8')) + i += 1 + for sk in gpgmeKey.subkeys: + if not sk.fpr.startswith(subkey.fpr): + ret += u"sub %sR/%s %s" % (sk.length, sk.fpr[-8:], sk.timestamp) + if sk.expires: ret += u' [expires: %s]\n' % (sk.expires,) + return ret + + def UIDExport_gpgme(uid, keydata): set_up_temp_dir() From db68dfbad843c9c6f387008d742639bb2e0815fb Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 15 Jul 2015 16:27:07 +0300 Subject: [PATCH 041/102] More code replacement with new gpg library calls. --- keysign/Sections.py | 4 ++-- keysign/SignPages.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 546345a..b390359 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -567,8 +567,8 @@ def on_button_clicked(self, button, *args, **kwargs): def recieved_key(self, fingerprint, keydata, *data): self.received_key_data = keydata - openpgpkey = self.tmpkeyring.get_keys(fingerprint).values()[0] - self.signPage.display_downloaded_key(openpgpkey, fingerprint) + gpgmeKey = gpg.gpg_get_keylist(self.ctx, fingerprint, False)[0] + self.signPage.display_downloaded_key(gpg.gpg_format_key(gpgmeKey)) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index 41eeaf4..c44206d 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -317,11 +317,11 @@ def __init__(self): self.pack_start(self.mainLabel, False, False, 0) - def display_downloaded_key(self, key, scanned_fpr): + def display_downloaded_key(self, formatted_key): # FIXME: If the two fingerprints don't match, the button # should be disabled - key_text = GLib.markup_escape_text(str(key)) + key_text = GLib.markup_escape_text(str(formatted_key)) markup = """\ From fcb79278cd9eda259ef754ae963f596d1e071849 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 16 Jul 2015 23:12:49 +0300 Subject: [PATCH 042/102] Sections: removed old gpg API which is not used anymore --- keysign/Sections.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index b390359..57e7b3b 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -31,8 +31,9 @@ from monkeysign.ui import MonkeysignUi from keysign.gpg import gpg -from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, GetNewTempKeyring, TempKeyringCopy from keysign.gpg.gpg import UIDExport_gpgme +from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, TempKeyringCopy + from compat import gtkbutton import Keyserver @@ -326,9 +327,6 @@ def try_download_keys(self, clients): def verify_downloaded_key(self, downloaded_data, fingerprint): # FIXME: implement a better and more secure way to verify the key - # FIXME2: We keep the calls to old API until we replaced it entirely - self.tmpkeyring.import_data(downloaded_data) - if gpg.gpg_import_keydata(self.ctx, downloaded_data): imported_key_fpr = gpg.gpg_get_keylist(self.ctx, None, False)[0].subkeys[0].fpr if imported_key_fpr == fingerprint: @@ -353,9 +351,6 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) other_clients = self.app.discovered_services self.log.debug("The clients found on the network: %s", other_clients) - #FIXME: should we create a new TempKeyring for each key we want - # to sign it ? - self.tmpkeyring = GetNewTempKeyring() # For each key downloaded we create a new gpgme.Context object and # set up a temporary dir for gpg self.ctx = gpgme.Context() From 0c69cca9623c4a25339b08fe06e6a3e5ef667897 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 18 Jul 2015 17:24:24 +0300 Subject: [PATCH 043/102] gpg: Some changes which give the library functions a more proper use. This prepares the code for a "sign key" function which will have to do the next steps: 1. Get the 'Primary Key' from the user's keyring, which will be used to sign. 2. For each UID of the key that must be signed: - check the sigs of that UID if it wasn't already signed by our key - if the UID matches the received UID , sign it using 'edit_sign' function from the gpgme.editutil module - return because we don't want to sign all UIDs If we want to adhere to our current workflow, we need to call this function for each UID of a key, and each call should import the key into a new context. In this way we can sign each UID independently, and then send a mail with the signed key. We do it like this for now as we don't yet have a 'del_uid' function. --- keysign/Sections.py | 2 +- keysign/gpg/gpg.py | 71 +++++++++++++++++++++++---------------------- 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 57e7b3b..c915960 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -354,7 +354,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) # For each key downloaded we create a new gpgme.Context object and # set up a temporary dir for gpg self.ctx = gpgme.Context() - gpg.gpg_set_dir(self.ctx) + gpg.gpg_set_engine(self.ctx) other_clients = self.sort_clients(other_clients, fingerprint) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 00b7a0c..0ad99bc 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -17,69 +17,65 @@ log = logging.getLogger() -orig_gpghome = os.environ['HOME'] + '/.gnupg/' -_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') +gpg_default = os.environ['HOME'] + '/.gnupg/' +gpg_path = find_executable('gpg') -def gpg_set_dir(gpgmeContext, dir_prefix=None): +def gpg_set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=None): """Sets up a temporary directory as new gnupg home for this context """ dir_prefix = dir_prefix if dir_prefix else 'tmp.gpghome' temp_dir = tempfile.mkdtemp(prefix=dir_prefix) + gpgmeContext.set_engine_info(protocol, gpg_path, temp_dir) + return temp_dir - gpg_path = find_executable('gpg') - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, temp_dir) - -def gpg_reset_dir(gpgmeContext): +def gpg_reset_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP): """Resets the gnupg dir to its default location for current context """ - default_gpghome = os.environ['HOME'] + '/.gnupg/' - gpg_path = find_executable('gpg') + gpgmeContext.set_engine_info(protocol, gpg_path, gpg_default) - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, default_gpghome) - -def copy_secrets(gpgmeContext): - """Copies secrets from .gnupg to temporary dir +def gpg_copy_secrets(gpgmeContext, gpg_homedir): + """Copies secrets from .gnupg to new @gpg_homedir """ + ctx = gpgme.Context() + try: - from_ = orig_gpghome + 'gpg.conf' - to_ = _gpghome + from_ = gpg_default + 'gpg.conf' + to_ = gpg_homedir shutil.copy(from_, to_) log.debug('copied your gpg.conf from %s to %s', from_, to_) except IOError as e: log.error('User has no gpg.conf file') - # Copy the public parts of the secret keys to the tmpkeyring - secret_keys = [key for key in gpgmeContext.keylist(None, True)] + secret_keys = [key for key in ctx.keylist(None, True)] + # We set again the gpg homedir because there is no contex method "get_engine_info" + # to tell us what gpg home it uses. + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpg_homedir) - gpgtemp = _gpghome for key in secret_keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: - import_key_by_fpr(gpgmeContext, key.subkeys[0].fpr, gpgtemp) + gpg_import_key_by_fpr(gpgmeContext, key.subkeys[0].fpr) -def import_key_by_fpr(gpgmeContext, fpr, new_homedir=None): - """Imports a key into a temporary keyring. +def gpg_import_key_by_fpr(gpgmeContext, fpr): + """Imports a key received by its @fpr into a temporary keyring. - It uses Context.set_engine_info() to restrict the change of gpg - directory to current context, elsewhere it would change it globally - through os.environ['GNUPGHOME'] + It assumes that the received @gpgmeContext has its gpg homedir set already. """ - new_homedir = new_homedir if new_homedir else tempfile.mkdtemp(prefix='tmp.gpghome') - gpg_path = find_executable('gpg') - - # The default context has access to user's default keyring + # We make a new context because we need to get the key from it ctx = gpgme.Context() keydata = extract_keydata(ctx, fpr, True) # It seems that keys can be imported from string streams only keydataIO = StringIO(keydata) - - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, new_homedir) - res = gpgmeContext.import_(keydataIO) + try: + res = gpgmeContext.import_(keydataIO) + except gpgme.GpgmeError as err: + log.error("No key found in user's keyring with fpr:\n%s", fpr) + raise ValueError('Invalid fingerprint') return len(res.imports) != 0 @@ -130,7 +126,7 @@ def extract_keydata(gpgmeContext, fpr, armor=False): Returns the data in plaintext (if armor=True) or binary. """ gpgmeContext.armor = armor - keydata = BytesIO() + keydata = StringIO() gpgmeContext.export(fpr, keydata) return keydata.getvalue() @@ -432,5 +428,12 @@ def keyring_get_keys(keyring, keyid=None): if __name__ == '__main__': - keyring = GetNewKeyring() - print keyring.get_keys() \ No newline at end of file + ctx = gpgme.Context() + gpghome = gpg_set_engine(ctx) + gpg_copy_secrets(ctx, gpghome) + + keys = gpg_get_keylist(ctx) + for key in keys: + key_str = gpg_format_key(key) + print ("\nKey: \n%s") %(key_str,) + From 8cff28ee4d7bbcdb9e1168df891ec9be88a734ca Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 18 Jul 2015 18:20:29 +0300 Subject: [PATCH 044/102] gpg: Deleted code that wasn't used anymore --- keysign/gpg/gpg.py | 126 +-------------------------------------------- 1 file changed, 1 insertion(+), 125 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 0ad99bc..7a0c475 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -6,7 +6,6 @@ import os import shutil import tempfile -from tempfile import NamedTemporaryFile from distutils.spawn import find_executable from monkeysign.gpg import Keyring, TempKeyring @@ -202,82 +201,8 @@ def UIDExport_gpgme(uid, keydata): remove_temp_dir() return keydata.getvalue() -class KeyringGPG: - """A class that has the functionalities of a keyring - It aggregates a gpgme.Context object through which executes gpg calls. - Aggregation is used because normal inheritance isn't possible. - """ - - default_gpghome = os.environ['HOME'] + '/.gnupg/' - - def __init__(self, tmphome = True): - self.log = logging.getLogger() - self._gpghome = None - - if tmphome: - self._gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') - self.set_up_tmp_dir() - - self.ctx = gpgme.Context() - - - def __del__(self): - if self._gpghome and not self._gpghome.startswith(default_gpghome): - del os.environ['GNUPGHOME'] - shutil.rmtree(self._gpghome, ignore_errors=True) - - - def set_up_tmp_dir(self): - os.environ['GNUPGHOME'] = self._gpghome - # Copy secrets from .gnupg to temporary dir - try: - from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' - to_ = self._gpghome - shutil.copy(from_, to_) - self.log.debug('copied your gpg.conf from %s to %s', from_, to_) - except IOError as e: - self.log.error('User has no gpg.conf file') - - - def import_data(self, keydata): - data = BytesIO(keydata) - try: - result = self.ctx.import_(data) - except gpgme.GpgmeError as err: - self.log.error("Couldn't import the key with the following keydata:\n%s", keydata) - return False - # XXX: we stick to return True/False for compatibility issues. - # The gpgme.ImportResult can be used to extract more information. - return True - - - def export_data(self, fpr, armor = True): - self.ctx.armor = armor - keydata = BytesIO() - self.ctx.export(fpr, keydata) - - return keydata.getvalue() - - - def get_key(self, fpr): - key = None - try: - key = self.ctx.get_key(fpr) - except gpgme.GpgmeError as err: - self.log.error('No key found with fpr %s', fpr) - raise ValueError('Invalid fingerprint') - - return key - - - def get_keylist(self, keyid = None, secret = False): - keys = [key for key in context.keylist(keyid, secret)] - return keys - - - -### From Sections.py ### +### Below are functions that use old API and must be replaced ### def UIDExport(uid, keydata): """Export only the UID of a key. @@ -378,55 +303,6 @@ def build_command(*args, **kwargs): -### Below functions represent gpg calls that mostly depend on -### monkeysign.gpg.Keyring and monkeysign.gpg.Context classes - -def keyring_set_option(keyring, option, value = None): - try: - if option in keyring.context.options: - keyring.context.set_option(option, value) - - except AttributeError: - log.error("Object %s has no attribute context", keyring) - except TypeError: - log.error("Object %s is not a Keyring type", keyring) - - -def keyring_call_command(keyring, command, stdin = None): - try: - if option in keyring.context.options: - keyring.context.call_command(command, stdin) - - except AttributeError: - log.error("Object %s has no attribute context", keyring) - except TypeError: - log.error("Object %s is not a Keyring type", keyring) - - -def keyring_import_data(keyring, data): - if keyring.import_data(data): - imported_key_fpr = keyring.get_keys().keys()[0] - log.debug("Imported data with fpr:\n%s", imported_key_fpr) - - else: - log.error("Couldn't import data:\n%s", data) - - -def keyring_export_data(keyring, keyid): - keys = keyring.get_keys(keyid) - key = keys.values()[0] - - keyring.export_data(key.fpr, secret=False) - keydata = keyring.context.stdout - return keydata - - -def keyring_get_keys(keyring, keyid=None): - keys_dict = keyring.get_keys(keyid) - return keys_dict - - - if __name__ == '__main__': ctx = gpgme.Context() gpghome = gpg_set_engine(ctx) From 8689d302b8f4d8e29cf4834094ceb128f949b804 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 21 Jul 2015 10:59:05 +0300 Subject: [PATCH 045/102] gpg: gpg_copy_secrets now copies the user's secring. This is needed because we can't sign a key without a primary key inside our temporary keyring. Unfortunately there is no '-export-secret-keys' option in gpgme API. The other way we could go was to import the key into the user's own keyring and delete it after we'd done with signing and exporting the key back. But this would have raised another problem, if the user already has the key in his keyring, then we would just delete it forever. --- keysign/gpg/gpg.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7a0c475..7295391 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -42,14 +42,18 @@ def gpg_copy_secrets(gpgmeContext, gpg_homedir): """ ctx = gpgme.Context() + secring_path = gpg_default + 'secring.gpg' + shutil.copy(secring_path, gpg_homedir) + log.debug('copied your secring.gpg from %s to %s', secring_path, gpg_homedir) + try: - from_ = gpg_default + 'gpg.conf' - to_ = gpg_homedir - shutil.copy(from_, to_) - log.debug('copied your gpg.conf from %s to %s', from_, to_) + conf_path = gpg_default + 'gpg.conf' + shutil.copy(conf_path, gpg_homedir) + log.debug('copied your gpg.conf from %s to %s', conf_path, gpg_homedir) except IOError as e: log.error('User has no gpg.conf file') + # Imports user's private keys into the new keyring secret_keys = [key for key in ctx.keylist(None, True)] # We set again the gpg homedir because there is no contex method "get_engine_info" # to tell us what gpg home it uses. From 16d5c47b30bb3761579895f2f5ddd424fa7aeaf4 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 21 Jul 2015 11:36:04 +0300 Subject: [PATCH 046/102] gpg: Added gpg_sign_uid which signs a given userId of a key For now we use an extra argument 'gpg_homedir' because gpgme.Context has no way to retrieve the gpg dir it uses to save data. The userId argument is a gpgme.UserId object and not a string uid. --- keysign/gpg/gpg.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7295391..b8c03c2 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -111,6 +111,49 @@ def gpg_get_keylist(gpgmeContext, keyid=None, secret=False): return keys +def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): + """Signs a specific uid of a OpenPGP key + + @gpg_homedir is the directory that @gpgmeContext uses for gpg. + @userId is a gpgme.UserId object + """ + # we import the user's primary key that will be used to sign + gpg_copy_secrets(gpgmeContext, gpg_homedir) + primary_key = [key for key in gpgmeContext.keylist(None, True)][0] + gpgmeContext.signers = [primary_key] + + try: + uid_name, uid_email, uid_comment = userId.name, userId.email, userId.comment + except AttributeError as exp: + log.error("%s is not a valid gpgme.UserId", userId) + raise ValueError("Invalid UID") + + # we set keylist mode so we can see signatures + gpgmeContext.keylist_mode = gpgme.KEYLIST_MODE_SIGS + key = gpgmeContext.get_key(userId.uid) + + i = 1 + # check if we didn't already signed this uid of the key + for uid in key.uids: + sigs = [sig for sig in uid.signatures if primary_key.subkeys[0].fpr.endswith(sig.keyid)] + + # check if this uid is the same with the one that we want to sign + if (uid.name.startswith(uid_name) and uid.email.startswith(uid_email) + and uid.comment.startswith(uid_comment)): + + if len(sigs) == 0: + gpgme.editutil.edit_sign(gpgmeContext, key, index=i, check=0) + + else: + # we already signed this UID + log.info("Uid %s was already signed by key: \n%s", userId.uid, key.subkeys[0].fpr) + return False + break + i += 1 + + return True + + def extract_fpr(gpgmeContext, keyid): """Extracts the fingerprint of a key with @keyid. """ From f6c84a1639eeea4d6080a1d49b340d03b363c458 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Wed, 22 Jul 2015 14:21:50 +0300 Subject: [PATCH 047/102] Made gpg_reset_engine to accept an optional argument as for the current gpg dir --- keysign/gpg/gpg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index b8c03c2..15fe7ad 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -30,11 +30,13 @@ def gpg_set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=Non return temp_dir -def gpg_reset_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP): +def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP): """Resets the gnupg dir to its default location for current context """ gpgmeContext.set_engine_info(protocol, gpg_path, gpg_default) + if tmp_dir: + shutil.rmtree(tmp_dir, ignore_errors=True) def gpg_copy_secrets(gpgmeContext, gpg_homedir): From bbbca3ed9bc8a8eda0090fa79e9d5fa36ec3b21e Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 11:22:16 +0300 Subject: [PATCH 048/102] gpg: Added gpg_encrypt_data function which will be used to encrypt keydata --- keysign/gpg/gpg.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 15fe7ad..5651fa6 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -156,6 +156,17 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): return True +def gpg_encrypt_data(gpgmeContext, data, uid, armor=True): + plaintext = BytesIO(data) + ciphertext = BytesIO() + gpgmeContext.armor = armor + recipients = gpg_get_keylist(gpgmeContext, uid) + + gpgmeContext.encrypt(recipients, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + return ciphertext.getvalue() + + def extract_fpr(gpgmeContext, keyid): """Extracts the fingerprint of a key with @keyid. """ From a71da8ec8d9ccc201db663a63f634766ef186806 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 11:49:32 +0300 Subject: [PATCH 049/102] Sections.py: Updated sign_key_async method to use our new gpg lib This was the last part in replacing the use of old API with new gpg API in Sections.py. --- keysign/Sections.py | 88 ++++++++++++++++++++++++--------------------- keysign/gpg/gpg.py | 1 + 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index c915960..8388fd5 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -31,8 +31,6 @@ from monkeysign.ui import MonkeysignUi from keysign.gpg import gpg -from keysign.gpg.gpg import UIDExport_gpgme -from keysign.gpg.gpg import UIDExport, MinimalExport, GetNewKeyring, TempKeyringCopy from compat import gtkbutton @@ -217,7 +215,6 @@ def __init__(self, app): self.log = logging.getLogger() # the temporary keyring we operate in - self.tmpkeyring = None self.ctx = None self.scanPage = ScanFingerprintPage() @@ -392,52 +389,48 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.debug("I will sign key with fpr {}".format(fingerprint)) - keyring = GetNewKeyring() - keyring.context.set_option('export-options', 'export-minimal') + ctx = gpgme.Context() + gpg_homedir = gpg.gpg_set_engine(ctx) - tmpkeyring = TempKeyringCopy(keyring) - - # 1. fetch the key into a temporary keyring - # 1.a) from the local keyring - # FIXME: WTF?! How would the ring enter the keyring in first place?! keydata = data or self.received_key_data - - if keydata: - stripped_key = MinimalExport(keydata) - else: # Do we need this branch at all? + # FIXME: until we have our patch to PyGPGME integrated, we cannot + # export the key with 'export-minimal' option + # stripped_key = gpg.gpg_export_minimal(ctx, keydata) + if not keydata: self.log.debug("looking for key %s in your keyring", fingerprint) - keyring.context.set_option('export-options', 'export-minimal') - stripped_key = keyring.export_data(fingerprint) - - self.log.debug('Trying to import key\n%s', stripped_key) - if tmpkeyring.import_data(stripped_key): - # 3. for every user id (or all, if -a is specified) - # 3.1. sign the uid, using gpg-agent - keys = tmpkeyring.get_keys(fingerprint) + default_ctx = gpgme.Context() + keydata = gpg.extract_keydata(default_ctx, fingerprint, True) + + # 1. Fetch the key into a temporary keyring + self.log.debug('Trying to import key\n%s', keydata) + if gpg.gpg_import_keydata(ctx, keydata): + + keys = gpg.gpg_get_keylist(ctx, fingerprint) self.log.info("Found keys %s for fp %s", keys, fingerprint) assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) - key = keys[fingerprint] - uidlist = key.uidslist - - # FIXME: For now, we sign all UIDs. This is bad. - ret = tmpkeyring.sign_key(uidlist[0].uid, signall=True) - self.log.info("Result of signing %s on key %s: %s", uidlist[0].uid, fingerprint, ret) + key = keys[0] + uidlist = key.uids + # 2. Sign each UID individually for uid in uidlist: uid_str = uid.uid self.log.info("Processing uid %s %s", uid, uid_str) - # 3.2. export and encrypt the signature - # 3.3. mail the key to the user - signed_key = UIDExport(uid_str, tmpkeyring.export_data(uid_str)) + res = gpg.gpg_sign_uid(ctx, gpg_homedir, uid) + if not res: + # we may have already signed this uid before + self.log.info("Uid %s couldn't be signed. Maybe we already signed it?", uid_str) + continue + + # 3. Export and encrypt the signature + signed_key = gpg.extract_keydata(ctx, fingerprint, True) self.log.info("Exported %d bytes of signed key", len(signed_key)) - # self.signui.tmpkeyring.context.set_option('armor') - tmpkeyring.context.set_option('always-trust') - encrypted_key = tmpkeyring.encrypt_data(data=signed_key, recipient=uid_str) - keyid = str(key.keyid()) - ctx = { + encrypted_key = gpg.gpg_encrypt_data(ctx, signed_key, uid_str) + + keyid = key.subkeys[0].fpr[-8:] + template_ctx = { 'uid' : uid_str, 'fingerprint': fingerprint, 'keyid': keyid, @@ -459,25 +452,40 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): # As we're done with the file, we close it. #tmpfile.close() - subject = Template(SUBJECT).safe_substitute(ctx) - body = Template(BODY).safe_substitute(ctx) + # mail the key to the user + subject = Template(SUBJECT).safe_substitute(template_ctx) + body = Template(BODY).safe_substitute(template_ctx) self.email_file (to=uid_str, subject=subject, body=body, files=[filename]) - # FIXME: Can we get rid of self.tmpfiles here already? Even if the MUA is still running? + # we have to re-import the key to have each UID signed individually + try: + ctx.delete(key) + except gpgme.GpgmeError as exp: + self.log.debug('You are signing one of your own keys: %s', key.subkeys[0].fpr) + ctx.delete(key, True) + + gpg.gpg_import_keydata(ctx, keydata) + keys = gpg.gpg_get_keylist(ctx, fingerprint) + self.log.info("Found keys %s for fp %s", keys, fingerprint) + assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) + key = keys[0] + + # FIXME: Can we get rid of self.tmpfiles here already? Even if the MUA is still running? # 3.4. optionnally (-l), create a local signature and import in # local keyring # 4. trash the temporary keyring - else: self.log.error('data found in barcode does not match a OpenPGP fingerprint pattern: %s', fingerprint) if error_cb: GLib.idle_add(error_cb, data) + # We are done signing the key so we remove the temporary keyring + gpg.gpg_reset_engine(ctx, gpg_homedir) return False diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 5651fa6..7beeb5e 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -10,6 +10,7 @@ from monkeysign.gpg import Keyring, TempKeyring import gpgme +import gpgme.editutil from io import BytesIO from StringIO import StringIO From b3507ef7e383e15042946d70571ac3383f09902b Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 13:14:45 +0300 Subject: [PATCH 050/102] Deleted unused UIDExport_gpgme func and cleaned up code. --- keysign/Sections.py | 8 ++------ keysign/gpg/gpg.py | 32 -------------------------------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 8388fd5..93d5127 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -31,7 +31,7 @@ from monkeysign.ui import MonkeysignUi from keysign.gpg import gpg - +import gpgme from compat import gtkbutton import Keyserver @@ -49,11 +49,7 @@ # Needed for window.get_xid(), xvimagesink.set_window_handle(), respectively: from gi.repository import GstVideo -import gpgme -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO + Gst.init([]) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 7beeb5e..dca8663 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -232,39 +232,7 @@ def gpg_format_key(gpgmeKey): return ret -def UIDExport_gpgme(uid, keydata): - set_up_temp_dir() - - ctx = gpgme.Context() - # XXX: do we need to set a "always-trust" flag ? - data = BytesIO(keydata) - try: - result = ctx.import_(data) - except gpgme.GpgmeError as err: - log.error("Couldn't import the key with the following keydata:\n%s", keydata) - raise ValueError('Invalid keydata') - - keys = [key for key in ctx.keylist(uid)] - for key in keys: - for u in key.uids: - # XXX: at this moment I don't know a way to delete other UIDs from the key - # so in the end we have only the uid that must be signed - if u != uid: - log.info('Deleting UID %s from key %s', u.uid, key.subkeys[0].fpr) - try: - key.uids.remove(u) - except ValueError as err: - log.error("Couldn't delete UID %s from key %s", u.uid, key.subkeys[0].fpr) - - keydata = BytesIO() - ctx.export(uid, keydata) - - remove_temp_dir() - return keydata.getvalue() - - ### Below are functions that use old API and must be replaced ### - def UIDExport(uid, keydata): """Export only the UID of a key. Unfortunately, GnuPG does not provide smth like From d66d408868cabb886e21f401973a9e12eb90fd36 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 13:16:05 +0300 Subject: [PATCH 051/102] gpg_encrypt_data: added docstrings for function --- keysign/gpg/gpg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index dca8663..9c4e787 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -158,6 +158,8 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): def gpg_encrypt_data(gpgmeContext, data, uid, armor=True): + """Encrypts @data for the recipients @uid + """ plaintext = BytesIO(data) ciphertext = BytesIO() gpgmeContext.armor = armor From 19af4a2ceebf4924b8393b97a8c3ee9cffe52c4c Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 17:22:12 +0300 Subject: [PATCH 052/102] Sections: we now remove the temporary dir created when we download keydata --- keysign/Sections.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 93d5127..8b95e7c 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -347,7 +347,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) # For each key downloaded we create a new gpgme.Context object and # set up a temporary dir for gpg self.ctx = gpgme.Context() - gpg.gpg_set_engine(self.ctx) + tmp_gpghome = gpg.gpg_set_engine(self.ctx, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix='tmp.gpghome') other_clients = self.sort_clients(other_clients, fingerprint) @@ -376,6 +376,10 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) self.log.debug('Adding %s as callback', callback) GLib.idle_add(callback, fingerprint, keydata, data) + # Remove the temporary keyring + gpg.gpg_reset_engine(self.ctx, tmp_gpghome) + self.log.info("Deleting temporary gpg home dir: %s", tmp_gpghome) + # If this function is added itself via idle_add, then idle_add will # keep adding this function to the loop until this func ret False return False @@ -482,6 +486,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): # We are done signing the key so we remove the temporary keyring gpg.gpg_reset_engine(ctx, gpg_homedir) + self.log.info("Deleting temporary gpg home dir: %s", gpg_homedir) return False From 6fb2255bec45465bdbba0648b49c52ccf9e7ca14 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 23 Jul 2015 18:18:20 +0300 Subject: [PATCH 053/102] KeysPage.py: replaced old gpg API --- keysign/KeysPage.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/keysign/KeysPage.py b/keysign/KeysPage.py index 034f5bc..63c53c1 100644 --- a/keysign/KeysPage.py +++ b/keysign/KeysPage.py @@ -26,9 +26,10 @@ from gi.repository import Gtk, GLib from gi.repository import GObject -from keysign.gpg.gpg import GetNewKeyring +import gpgme # These are relative imports +from gpg import gpg from __init__ import __version__ log = logging.getLogger() @@ -71,19 +72,18 @@ def __init__(self, show_public_keys=False): # TreeView, i.e. in get_items_from_selection. self.store = Gtk.ListStore(str, str, str) - # FIXME: this should be moved to KeySignSection - self.keyring = GetNewKeyring() # the user's keyring + # XXX: maybe we can move this to KeySignSection in Sections.py + self.ctx = gpgme.Context() self.keysDict = {} - # FIXME: this should be a callback function to update the display - # when a key is changed/deleted - for key in self.keyring.get_keys(None, secret=True, public=show_public_keys).values(): - if key.invalid or key.disabled or key.expired or key.revoked: + # FIXME: implement a callback that refreshes the UIDs when they change + for key in gpg.gpg_get_keylist(self.ctx, None, True): + if key.revoked or key.expired or key.invalid or key.subkeys[0].disabled: continue - uidslist = key.uidslist #UIDs: Real Name (Comment) - keyid = str(key.keyid()) # the key's short id + uidslist = key.uids + keyid = key.subkeys[0].fpr[-8:] if not keyid in self.keysDict: self.keysDict[keyid] = key @@ -178,7 +178,7 @@ def on_selection_changed(self, selection, *args): self.emit('key-selection-changed', keyid) try: - exp_date = datetime.fromtimestamp(float(key.expiry)) + exp_date = datetime.fromtimestamp(float(key.subkeys[0].expires)) expiry = "{:%Y-%m-%d %H:%M:%S}".format(exp_date) except ValueError, e: expiry = "No expiration date" @@ -220,7 +220,7 @@ def on_publish_button_clicked(self, button, key, *args): to publish a key on the network. It will emit a "key-selected" signal with the ID of the selected key.''' log.debug('Clicked publish for key (%s) %s (%s)', type(key), key, args) - keyid = key.keyid() + keyid = key.subkeys[0].fpr[-8:] self.emit('key-selected', keyid) From 88ad53b62506089e9c9a6e307b29e925571e5aa4 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 24 Jul 2015 13:45:40 +0300 Subject: [PATCH 054/102] Added gpg_get_siglist lib function and more API replacement in SignPages.py --- keysign/SignPages.py | 53 +++++++++++++++++--------------------------- keysign/gpg/gpg.py | 14 ++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index c44206d..7c89597 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -23,9 +23,11 @@ import StringIO from gi.repository import GObject, Gtk, GLib, GdkPixbuf -from keysign.gpg.gpg import GetNewKeyring from qrencode import encode_scaled +import gpgme +from gpg import gpg + from datetime import datetime from compat import gtkbutton @@ -36,43 +38,31 @@ log = logging.getLogger() -def parse_sig_list(text): - '''Parses GnuPG's signature list (i.e. list-sigs) - - The format is described in the GnuPG man page''' - sigslist = [] - for block in text.split("\n"): - if block.startswith("sig"): - record = block.split(":") - log.debug("sig record (%d) %s", len(record), record) - keyid, timestamp, uid = record[4], record[5], record[9] - sigslist.append((keyid, timestamp, uid)) - - return sigslist - # This is a cache for a keyring object, so that we do not need # to create a new object every single time we parse signatures -_keyring = None -def signatures_for_keyid(keyid, keyring=None): +_context = None +def signatures_for_keyid(keyid, context=None): '''Returns the list of signatures for a given key id - This will call out to GnuPG list-sigs, using Monkeysign, - and parse the resulting string into a list of signatures. + This calls gpg.get_sig_list which returns a list with all + sigs for this uid A default Keyring will be used unless you pass an instance as keyring argument. ''' # Retrieving a cached instance of a keyring, # unless we were being passed a keyring - global _keyring - if keyring is not None: - kr = keyring + global _context + if context is not None: + ctx = context else: - kr = _keyring if _keyring else GetNewKeyring() + ctx = _context if _context else gpgme.Context() + + sigs = gpg.gpg_get_siglist(ctx, keyid) - # FIXME: this would be better if it was done in monkeysign - kr.context.call_command(['list-sigs', keyid]) - siglist = parse_sig_list(kr.context.stdout) + siglist = [] + for sig in sigs: + siglist.append((sig.keyid, sig.timestamp, sig.uid)) return siglist @@ -158,9 +148,6 @@ def __init__(self): self.set_spacing(10) self.log = logging.getLogger() - # FIXME: this should be moved to KeySignSection - self.keyring = GetNewKeyring() - uidsLabel = Gtk.Label() uidsLabel.set_text("UIDs") @@ -183,7 +170,7 @@ def __init__(self): self.pack_start(self.signaturesBox, True, True, 0) - def display_uids_signatures_page(self, openPgpKey): + def display_uids_signatures_page(self, gpgmeKey): # destroy previous uids for uid in self.uidsBox.get_children(): @@ -193,7 +180,7 @@ def display_uids_signatures_page(self, openPgpKey): # display a list of uids labels = [] - for uid in openPgpKey.uidslist: + for uid in gpgmeKey.uids: label = Gtk.Label(str(uid.uid)) label.set_line_wrap(True) labels.append(label) @@ -203,7 +190,7 @@ def display_uids_signatures_page(self, openPgpKey): label.show() try: - exp_date = datetime.fromtimestamp(float(openPgpKey.expiry)) + exp_date = datetime.fromtimestamp(float(gpgmeKey.subkeys[0].expires)) expiry = "Expires {:%Y-%m-%d %H:%M:%S}".format(exp_date) except ValueError, e: expiry = "No expiration date" @@ -212,7 +199,7 @@ def display_uids_signatures_page(self, openPgpKey): ### Set up signatures - keyid = str(openPgpKey.keyid()) + keyid = str(gpgmeKey.subkeys[0].fpr[-8:]) sigslist = signatures_for_keyid(keyid) SHOW_SIGNATURES = False diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 9c4e787..05e1e42 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -114,6 +114,20 @@ def gpg_get_keylist(gpgmeContext, keyid=None, secret=False): return keys +def gpg_get_siglist(gpgmeContext, keyid): + '''Returns a list with all signatures for this @keyid + ''' + siglist = set() + gpgmeContext.keylist_mode = gpgme.KEYLIST_MODE_SIGS + key = gpgmeContext.get_key(keyid) + + for uid in key.uids: + sigs = [sig for sig in uid.signatures] + siglist = siglist.union(sigs) + + return list(siglist) + + def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): """Signs a specific uid of a OpenPGP key From d62e72aabf9715c99788b3d915da5f405e1182e1 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 24 Jul 2015 13:56:45 +0300 Subject: [PATCH 055/102] KeyPresent: replaced old gpg API --- keysign/KeyPresent.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/keysign/KeyPresent.py b/keysign/KeyPresent.py index d00ddbc..d8332d5 100644 --- a/keysign/KeyPresent.py +++ b/keysign/KeyPresent.py @@ -26,7 +26,8 @@ from gi.repository import Gtk, GLib from gi.repository import GObject -from keysign.gpg.gpg import GetNewKeyring +import gpgme +from gpg import gpg # These are relative imports from __init__ import __version__ @@ -130,13 +131,11 @@ def main(args=sys.argv): #if arguments.gpg: # keyid = arguments.file - # keyring = GetNewKeyring() - # # this is a dict {fpr: key-instance} - # found_keys = keyring.get_keys(keyid) + # ctx = gpgme.Context() + # found_keys = gpg.gpg_get_keylist(ctx, keyid) # # We take the first item we found and export the actual keydata - # fpr = found_keys.items()[0][0] - # keyring.export_data(fpr=fpr, secret=False) - # keydata = keyring.context.stdout + # fpr = found_keys[0].subkeys[0].fpr + # keydata = gpg.extract_keydata(ctx, fpr, True) #else: # keydata = open(arguments.file, 'r').read() fpr = arguments.fpr From 32f216be666d9075d47614bb995072ca5277819b Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 24 Jul 2015 15:55:45 +0300 Subject: [PATCH 056/102] gpg: Finished the replacement of monkeysign with gpgme. This gpg library was made to allow us to work with GnuPG from Python. Some of the most important functionalities that our library offers are: * set up and use context objects with temporary dirs as the gpg home * import keys from strings with keydata * sign a specific UID of a key ony * encrypt keydata * extracts fpr or keydata from a key * display keys in a fashion way --- keysign/gpg/gpg.py | 112 ++++----------------------------------------- 1 file changed, 8 insertions(+), 104 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 05e1e42..1b05d97 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -8,7 +8,6 @@ import tempfile from distutils.spawn import find_executable -from monkeysign.gpg import Keyring, TempKeyring import gpgme import gpgme.editutil from io import BytesIO @@ -248,113 +247,18 @@ def gpg_format_key(gpgmeKey): return ret -### Below are functions that use old API and must be replaced ### -def UIDExport(uid, keydata): - """Export only the UID of a key. - Unfortunately, GnuPG does not provide smth like - --export-uid-only in order to obtain a UID and its - signatures.""" - tmp = TempKeyring() - # Hm, apparently this needs to be set, otherwise gnupg will issue - # a stray "gpg: checking the trustdb" which confuses the gnupg library - tmp.context.set_option('always-trust') - tmp.import_data(keydata) - for fpr, key in tmp.get_keys(uid).items(): - for u in key.uidslist: - key_uid = u.uid - if key_uid != uid: - log.info('Deleting UID %s from key %s', key_uid, fpr) - tmp.del_uid(fingerprint=fpr, pattern=key_uid) - only_uid = tmp.export_data(uid) - - return only_uid - - -def MinimalExport(keydata): - '''Returns the minimised version of a key - - For now, you must provide one key only.''' - tmpkeyring = TempKeyring() - ret = tmpkeyring.import_data(keydata) - log.debug("Returned %s after importing %s", ret, keydata) - assert ret - tmpkeyring.context.set_option('export-options', 'export-minimal') - keys = tmpkeyring.get_keys() - log.debug("Keys after importing: %s (%s)", keys, keys.items()) - # We assume the keydata to contain one key only - fingerprint, key = keys.items()[0] - stripped_key = tmpkeyring.export_data(fingerprint) - return stripped_key - - -def GetNewKeyring(): - return Keyring() - -def GetNewTempKeyring(): - return TempKeyring() - -class TempKeyringCopy(TempKeyring): - """A temporary keyring which uses the secret keys of a parent keyring - - It mainly copies the public keys from the parent keyring to this temporary - keyring and sets this keyring up such that it uses the secret keys of the - parent keyring. - """ - def __init__(self, keyring, *args, **kwargs): - self.keyring = keyring - # Not a new style class... - if issubclass(self.__class__, object): - super(TempKeyringCopy, self).__init__(*args, **kwargs) - else: - TempKeyring.__init__(self, *args, **kwargs) - - self.log = logging.getLogger() - - tmpkeyring = self - # Copy and paste job from monkeysign.ui.prepare - tmpkeyring.context.set_option('secret-keyring', keyring.homedir + '/secring.gpg') - - # copy the gpg.conf from the real keyring - try: - from_ = keyring.homedir + '/gpg.conf' - to_ = tmpkeyring.homedir - shutil.copy(from_, to_) - self.log.debug('copied your gpg.conf from %s to %s', from_, to_) - except IOError as e: - # no such file or directory is alright: it means the use - # has no gpg.conf (because we are certain the temp homedir - # exists at this point) - if e.errno != 2: - pass - - - # Copy the public parts of the secret keys to the tmpkeyring - signing_keys = [] - for fpr, key in keyring.get_keys(None, secret=True, public=False).items(): - if not key.invalid and not key.disabled and not key.expired and not key.revoked: - signing_keys.append(key) - tmpkeyring.import_data (keyring.export_data (fpr)) - - -## Monkeypatching to get more debug output -import monkeysign.gpg -bc = monkeysign.gpg.Context.build_command -def build_command(*args, **kwargs): - ret = bc(*args, **kwargs) - #log.info("Building command %s", ret) - log.debug("Building cmd: %s", ' '.join(["'%s'" % c for c in ret])) - return ret -monkeysign.gpg.Context.build_command = build_command +def test_print_secret_keys(gpgmeContext): + gpg_copy_secrets(gpgmeContext, gpghome) + keys = gpg_get_keylist(gpgmeContext) + for key in keys: + key_str = gpg_format_key(key) + print ("\nKey: \n%s") %(key_str,) if __name__ == '__main__': ctx = gpgme.Context() gpghome = gpg_set_engine(ctx) - gpg_copy_secrets(ctx, gpghome) - - keys = gpg_get_keylist(ctx) - for key in keys: - key_str = gpg_format_key(key) - print ("\nKey: \n%s") %(key_str,) + test_print_secret_keys(ctx) + gpg_reset_engine(ctx, gpghome) From 577ecaf9b2d18feac4b0937fabd34ea73279a936 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 2 Aug 2015 22:10:48 +0300 Subject: [PATCH 057/102] gpg: deleted extra test code from the library --- keysign/gpg/example.py | 91 --------------------------------------- keysign/gpg/gpg.py | 17 -------- keysign/gpg/keys/key1.pub | 27 ------------ keysign/gpg/keys/key2.pub | 20 --------- 4 files changed, 155 deletions(-) delete mode 100644 keysign/gpg/example.py delete mode 100644 keysign/gpg/keys/key1.pub delete mode 100644 keysign/gpg/keys/key2.pub diff --git a/keysign/gpg/example.py b/keysign/gpg/example.py deleted file mode 100644 index 7301a11..0000000 --- a/keysign/gpg/example.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python - -import os -import shutil -import tempfile -import logging - -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO - -import gpgme - - -log = logging.getLogger() - -keydir = os.path.join(os.path.dirname(__file__), 'keys') -test1_fpr = '140162A978431A0258B3EC24E69EEE14181523F4' -test2_fpr = 'D09A409FC49466806CF79837946B842CDB8CFC4E' - -_gpghome = tempfile.mkdtemp(prefix='tmp.gpghome') - -def set_up_temp_dir(): - os.environ['GNUPGHOME'] = _gpghome - # Copy secrets from .gnupg to temporary dir - try: - from_ = os.environ['HOME'] + '/.gnupg/gpg.conf' - to_ = _gpghome - shutil.copy(from_, to_) - log.debug('copied your gpg.conf from %s to %s', from_, to_) - except IOError as e: - log.error('User has no gpg.conf file') - -def remove_temp_dir(): - del os.environ['GNUPGHOME'] - shutil.rmtree(_gpghome, ignore_errors=True) - - -def keyfile(key): - return open(os.path.join(keydir, key), 'rb') - - -def import_data(context, keydata): - result = context.import_(keydata) - return result - - -def export_key(context, fpr, armor=True): - context.armor = armor - keydata = BytesIO() - context.export(fpr, keydata) - return keydata.getvalue() - - -def get_key(context, fpr): - key = None - try: - key = context.get_key(fpr) - except gpgme.GpgmeError as err: - log.error('No key found with fpr %s', fpr) - raise ValueError('Invalid fingerprint') - - return key - - -def get_keys_iterator(context, keyid = None, secret = False): - keys = [key for key in context.keylist(keyid, secret)] - return keys - - -def main(): - # set up the environment - set_up_temp_dir() - ctx = gpgme.Context() - - # test key import - with keyfile('key1.pub') as fp: - result = import_data(ctx, fp) - assert result.imports[0] == (test_fpr, None, gpgme.IMPORT_NEW), "Fail on import" - - # test key export - keydata = export_key(ctx, result.imports[0][0]) - assert keydata.getvalue().startswith( - b'-----BEGIN PGP PUBLIC KEY BLOCK-----\n'), "Fail on export" - - # clean testing environment - remove_temp_dir() - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 1b05d97..d3206ac 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -245,20 +245,3 @@ def gpg_format_key(gpgmeKey): ret += u"sub %sR/%s %s" % (sk.length, sk.fpr[-8:], sk.timestamp) if sk.expires: ret += u' [expires: %s]\n' % (sk.expires,) return ret - - -def test_print_secret_keys(gpgmeContext): - gpg_copy_secrets(gpgmeContext, gpghome) - - keys = gpg_get_keylist(gpgmeContext) - for key in keys: - key_str = gpg_format_key(key) - print ("\nKey: \n%s") %(key_str,) - - -if __name__ == '__main__': - ctx = gpgme.Context() - gpghome = gpg_set_engine(ctx) - test_print_secret_keys(ctx) - gpg_reset_engine(ctx, gpghome) - diff --git a/keysign/gpg/keys/key1.pub b/keysign/gpg/keys/key1.pub deleted file mode 100644 index 8f18098..0000000 --- a/keysign/gpg/keys/key1.pub +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.14 (GNU/Linux) - -mI0EU6hgvAEEAMSNolGLgpPJQxLGTTn/Fr+RsWBXeOZCkMg5Bd/V0vuZNYCDKL9T -KhxQ/M9ZrxMciE2QTV932TbNgHttHV0j9Vot92wssyXfHnZ1elNmTSLIhezVVpRg -9fNkQHVG+noJBws7P8y/6sktcTpC6tc9Qq2FeoickcqbnH9PP8sWQy6/ABEBAAG0 -W0FuZHJlaSBHYWJyaWVsIChUaGlzIGlzIGEgdGVtcG9yYXJ5IGtleSwgb25seSBm -b3IgdGVzdGluZyBwdXJwb3NlLikgPGtoZW9wc190aGUxQHlhaG9vLmNvbT6IuAQT -AQIAIgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AFAlWF6/AACgkQ5p7uFBgV -I/RQFQQAu6niOcBSKl8qtDbb+cGnsxU0QsjI3mMR4cdaoWXyruSzDllIGEAuV2rh -yjn2DfOBhiZZWzqgnw2PB8UM4JvbS+xEthaRoNRUYnQ+pE94ftvjDrCtAZCvAq0y -szD1dCfNmI9WJp048L8nlR2nxILejJVtgNOC18LMmGbizcUZsVmJARwEEAECAAYF -AlOoZqMACgkQSwMtPe2DEqJozQf/Q2lvY6qeVHTj6lsj/SyLhgKsZ5aGzBNUpWLp -0anP8EvFdQGM5JxN5hyV5oRzS2LsNJyV4S9NWVt3q6Xwu6/mA4P4cCwP9RXP6osZ -7uGWnTs+p+MECimd+rCoquwltRJs8JmQVNXQS55bcNRGfTc8OBYhdaXSdVKGSS5A -4y/spmxX0QEAIgbqzxmyz90LfEvyQcFOPwD4U2U5FmEFVmBxV2GTyRSv2ol/59af -RT0xItyTmJ/MWgVPy5UMwxlW6dIO5A9zeStR2S0VDe/LEZr/PhY2Ib6qwV4MhPZ7 -thLjOfN4GAvS7Lxi1JH4phe2U8cGyJCGwnBwpkV4hgFcZBTGdriNBFOoYLwBBACr -XuaBpuVeuhfQ4YksW3vpYS+aI0pryIIG+xQ07U0O2PJwmakKcZCIxvI/D8d/vQGH -+MjgHYw/LWyoQgF0nt5RdKEuV+n2CwgiihCzT4DtnUJVhcXiBSmTS3zsoSsENuy8 -qgJZfTqsk8VtN+iyGhVBvHh4GplAy4VEbn0FuaCcwQARAQABiJ8EGAECAAkCGwwF -AlWF7FIACgkQ5p7uFBgVI/RyXgP5AWZhQhhmnPPh+TSiIeYK8xbJPAEcU076aYQv -8B4UlooZQo3jakoEpdFprGCR//bCLLDhKpUA3/3o1VmGo54LCbm2y/U1UC2m70fm -ZmfR1DbkE06FM0GR9M/5r6vwyNZ3TFwiwa5zk6HsMHxw4ck5J14n3DDPw9V6hMNX -ieS7wgs= -=GEgu ------END PGP PUBLIC KEY BLOCK----- diff --git a/keysign/gpg/keys/key2.pub b/keysign/gpg/keys/key2.pub deleted file mode 100644 index 9d510e4..0000000 --- a/keysign/gpg/keys/key2.pub +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.14 (GNU/Linux) - -mI0EVYxdtQEEAJwXn/1s5EPES52H2rWwZIXS1rlf3o6y9bOTO6612Gfa83OZaIXF -00CIwZKGcFxHG4mV5Y4RPA67pz85ZRU1jt1a5D64cml16Qi/dOVoyRaBMtYiIWpn -oTlRS4f/3nf8yzDlaw/H+SPh7BzwGJTAJg2cP3ZdOMT6+6Ymi6pHahL/ABEBAAG0 -J0pvaG4gRG9lIChUZXN0IGtleSkgPGpvaG4uZG9lQHRlc3QuY29tPoi+BBMBAgAo -BQJVjF21AhsDBQkCMFubBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCUa4Qs -24z8Tv3WA/9P8GFg/Uuck6pXovxk/jr5vK36wW/+5BtlpN8MzqhEDk8z99gawbmQ -icYRx5gz1BjVQWx+kAXTJdlndMmOAXEMj2dzgh9Unom2b0eHLeRcXJQuc74cxCi/ -HLLrEOGK0RkSdqPrfJnzjT1RMGPRiqkSFuFO14kknvbJC9RjiebOX7iNBFWMXbUB -BADHUhCjBcTccShlKU/9Y7sO40BhvYfSV7UDXWzquTgvOK1ZWr5HaO47TNfKEPOw -+wTwnMwEn+MpLF2kvnSIUoG/i3kh6J3+5dIeRqk5M16GCABooP5H+ssDyVgLcoYa -cLdvyCf6wxjwgx5UbT+Y9KC27ruwiELHteZQ7+I+PNO6SQARAQABiKUEGAECAA8F -AlWMXbUCGwwFCQIwW5sACgkQlGuELNuM/E55EgQAmarSCXVt3Gsz+TWhMoB/2foj -mMheTVD5cp2vpoG24fIjwIN/q2ZGQb3roRtUOCyLmDyh6c8LOG1NFyVP8iVSR5uo -zZK8cpeptiGlUqM8WUTrxzrxhsd57oqKPS2UQZLhN4YcAvoYAIAArh4Bc+WijY0n -1jywSvDM7oCdjEKy4OM= -=jzft ------END PGP PUBLIC KEY BLOCK----- From 30cb394afa61590d4418578a772978fe571e190f Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 2 Aug 2015 23:19:20 +0300 Subject: [PATCH 058/102] gpg: added a test suite for gpg library --- keysign/gpg/keys/testkey1.pub | 31 ++++++++ keysign/gpg/keys/testkey2.pub | 20 ++++++ keysign/gpg/test.py | 128 ++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 keysign/gpg/keys/testkey1.pub create mode 100644 keysign/gpg/keys/testkey2.pub create mode 100644 keysign/gpg/test.py diff --git a/keysign/gpg/keys/testkey1.pub b/keysign/gpg/keys/testkey1.pub new file mode 100644 index 0000000..3f36ef2 --- /dev/null +++ b/keysign/gpg/keys/testkey1.pub @@ -0,0 +1,31 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQENBFW6eXgBCADY8dYFEmz1hh4sjMNZ8bC5jjJaVE0pv34ATX0QJdaiLJZCzZeY ++Ly5Sndxi0Jx0DlmkXGQrDuPNBjJPr4wS5ea5CZG2VD8flq+GkgskawPRAQQ4xTs +Yrgoo7AwTJVK2E05c9/rdV+ksHXray2XJc1XsQ0gF4ZBbBTHXGzrj1jXxvuGNwm1 +AP7fSlQcBC90inSJVsMduYUFRGSihkTcSrENP7kYBGqL8DjQW2lzQdrtE1SFQtdR +uRyuvznGExu/UkuspAZTft235mYmi2VQNsYmZ3qc7vZ7IiyXsF+ixgtS4fzCgyFA +ZoQe5kFt9vX9/u/SFRJSB9LBeP/OVFI5jZoDABEBAAG0KUpvaG4gRG9lIChUZXN0 +IGtleSAxKSA8am9obi5kb2VAdGVzdC5jb20+iQE+BBMBAgAoBQJVunl4AhsDBQkA +J40ABgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCbM8/Hu3Da+n5LB/9mL/vf +PJKAUxGC9fwiaPtB4avDBIgNbl3TpGZw9cPQ34Ze+B5chg4PwyLmhioCVHegtglR +SrhCgZcivoOq8jQKvc/hYrTZUB7TiQcCnvaPEDFtUbUpZHrfEAg9vr0CDIPjWnx8 +4e5ehiwA60LKsFyHyiE0ugltl89V8FxJmQfMUda6Nz3ZivScfpXJAsgMub3NRTJs +TBuTCIcc241gslCDp3SNfyE4JXTuMZlyGXiecrlane1xptbhqOVwelvJU0YbmwFg +zzUQ2yht0XPs5/5acVuYpSyPCzT3XrLbGo0sFyvPJi7WYGf4bvI9NyOxpDlUUKb1 +YVpkT4wqfPCx8HtVuQENBFW6eXgBCADhBtMPD6CHFn8MsuWjreBvVMCcDgQSSNOC +ieHsraUx93BarSYvHrp+qWpD//rE3rYp1MmZJTJt879bxnrEGOvVqcXqX4t8sGYj +tPR3Dndl/yvMBT/BGwHzGkjEGMSUby0bj/0IpbC4x+E0DMMn8C8TYONVGf5GpY7J +z37fev28Ya/Mt1QsWHrfLmeqwh71hrrAWgfM2hp/9meXSCCvTkriaKFRHJTlnemU +TEPvx5rI8lM4z5PuYehvLL2PkUGq0nK58QkAUIdGeiX1ych/EQrODxngBMHB4O91 +fU+RBaNmHZwMmYToBXa1CWqbSaBYuoyBFVGuNR3juEeVc5Lb1QHpABEBAAGJASUE +GAECAA8FAlW6eXgCGwwFCQAnjQAACgkQmzPPx7tw2vqhdQgA2GZKSYQjmsRSaEjP +/oqQhXsTsPTs4mNgUMgG2lLVi8+fiHWSLYfvrkNcaLT6RaKW3SqhFPMSMvf5WcgK +TIwmWlLPC+1dRcWw+JVOcTkgbZ9lWjP29XlJx30iM5pjVupB2/w7EZV/alAu2Tyv +7youIxROYi4rOt/7U+9qkctzRyJneWEi7YKCSAsqRuOp/lG20AcFdQLERYybbnhh +JVPHyMKItCj/TXMRxMVqz5xzLgDP3V7m/s3KpBriJJWhZE6mxMGphd1tdHHcVl+S +2MvqmwRdRDckMUt/6F5xYVX8YCU9HJHY29kflm6ElVbS4Du7GoP6yV/DytZJK/Me +LJaOZA== +=Dw+F +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keysign/gpg/keys/testkey2.pub b/keysign/gpg/keys/testkey2.pub new file mode 100644 index 0000000..4b1f7d1 --- /dev/null +++ b/keysign/gpg/keys/testkey2.pub @@ -0,0 +1,20 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mI0EVbqCqAEEAKYNDbu4zCTOtOb8tnODaA7WnBHM8rSFFR4Hyd+S6LYMKY3n89cn +sacrnsuYaOH8c+cQlLNBAI9jzerwm3WhV0xc/muUb3yfcUzTXxCXpKfizwms4dIY +dPmLID1MUjqORaBhKIyyzgRlPJoIzp7i9NVrAhq7TdhW73uk7lFFLkedABEBAAG0 +K0pvaG55IERvZSAoVGVzdCBrZXkgMikgPGpvaG55LmRvZUB0ZXN0LmNvbT6IvgQT +AQIAKAUCVbqCqAIbAwUJACeNAAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ +8ef033cVdY7PIAP+PUfTAdjyklsycxJYWPcfUQi/YS/GfhgPk5k02ktoCyYZD+ar +bOHvNxkYAXQwzJomx/XAWVcW2iFD5E/ObqmKP+lEf8Ef1/FRIWJja9qIZkRYZrkx +8DkHp7fcpZWI1MWBRd6DPbYFp4Pz4dCQKi+hO/EsQIMuUYn9o1ZJPxOUXgW4jQRV +uoKoAQQAtswckAKtMTz8MWZiD+iCPbiSLF3cWaTv3ZklB8952Nmzx7sfx1Kj7uzL +OyGnBH8p0USFIdpXLZqa0io1cZGms9hvHz0RybXQDu3Id363s6OrJY9XnGxyMwle +OTYHL9L6bkRmXbozkzjO22nZztJuviZywzTEASt7VcDpy3XwIXUAEQEAAYilBBgB +AgAPBQJVuoKoAhsMBQkAJ40AAAoJEPHn9N93FXWOuw0D/3kx4rNf5BDocA96L4kP +T9YrVgSs7N71wRPWxFrO2GCVP9K4jaflFxOZp93Me/wvJ7EcHB+R7LGFcaP29WfJ +h4bDszKoZFm40Wd9Uu7y72jXJdvp+oSRU8pFyBz4ZVRSSDJ6aVs4LeYihV0EOej3 +5syUccEA5wa96mTyAsj4/SzN +=VcPb +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py new file mode 100644 index 0000000..ac94c5e --- /dev/null +++ b/keysign/gpg/test.py @@ -0,0 +1,128 @@ +#!/bin/bash/env python + +import os +import sys +import shutil +import tempfile + +import gpgme +import gpg + +import unittest + +from io import BytesIO +from StringIO import StringIO + +__all__ = ['GpgTestSuite'] + +keydir = os.path.join(os.path.dirname(__file__), 'keys') +gpg_default = os.environ['HOME'] + '/.gnupg/' + + +class GpgTestSuite(unittest.TestCase): + + def keyfile(self, key): + return open(os.path.join(keydir, key), 'rb') + + def test_gpg_set_engine(self): + ctx = gpgme.Context() + + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + gpg.gpg_set_engine(ctx, gpgme.PROTOCOL_OpenPGP, tmpdir) + + keys = [key for key in ctx.keylist()] + + self.assertEqual(len(keys), 0) + self.assertEqual(ctx.protocol, gpgme.PROTOCOL_OpenPGP) + + # clean temporary dir + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_gpg_reset_engine(self): + ctx = gpgme.Context() + # set a temporary dir for gpg home + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + + # check if we have created the new gpg dir + self.assertTrue(os.path.isdir(tmpdir)) + gpg.gpg_reset_engine(ctx, tmpdir, gpgme.PROTOCOL_OpenPGP) + + self.assertEqual(gpgme.PROTOCOL_OpenPGP, ctx.protocol) + self.assertFalse(os.path.exists(tmpdir)) + + # clean temporary dir + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_gpg_copy_secrets(self): + ctx = gpgme.Context() + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + + gpg.gpg_copy_secrets(ctx, tmpdir) + + # get the user's secret keys + default_ctx = gpgme.Context() + default_ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, gpg_default) + default_secret_keys = [key for key in default_ctx.keylist(None, True)] + + # compare the imported keys with the original secret keys + secret_keys = [key for key in ctx.keylist(None, True)] + + self.assertEqual(len(secret_keys), len(default_secret_keys)) + i = 0 + for i in xrange(len(secret_keys)): + self.assertEqual(secret_keys[i].subkeys[0].fpr, default_secret_keys[i].subkeys[0].fpr) + i += 1 + # clean temporary dir + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_gpg_import_key_by_fpr(self): + ctx = gpgme.Context() + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + + with self.keyfile('testkey1.pub') as fp: + ctx.import_(fp) + + res = gpg.gpg_import_key_by_fpr(ctx, '31E91E906BA25D74BB315DEA9B33CFC7BB70DAFA') + self.assertTrue(res) + + # can we get the key ? + key = ctx.get_key('31E91E906BA25D74BB315DEA9B33CFC7BB70DAFA') + # clean temporary dir + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_gpg_import_keydata(self): + ctx = gpgme.Context() + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + + with self.keyfile('testkey1.pub') as fp: + keydata = fp.read() + + res = gpg.gpg_import_keydata(ctx, keydata) + self.assertTrue(res) + + # can we get the key ? + key = ctx.get_key('john.doe@test.com') + + def test_gpg_sign_uid(self): + ctx = gpgme.Context() + tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + + with self.keyfile('testkey1.pub') as fp: + ctx.import_(fp) + + userId = ctx.get_key('john.doe@test.com').uids[0] + res = gpg.gpg_sign_uid(ctx, tmpdir, userId) + self.assertTrue(res) + + # verify if we have the uid signed + sigs = ctx.get_key('john.doe@test.com').uids[0].signatures + self.assertEqual(len(sigs), 2) #we're counting the self signature + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From caf94b1c891e16ed2d82f75b13a8c15f9c61671b Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 2 Aug 2015 23:40:32 +0300 Subject: [PATCH 059/102] Sections.py: removed MonkeysignUI which wasn't used anymore. We have now completely removed monkeysign dependency. --- keysign/Sections.py | 65 --------------------------------------------- 1 file changed, 65 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 8b95e7c..71b127c 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -29,7 +29,6 @@ import sys -from monkeysign.ui import MonkeysignUi from keysign.gpg import gpg import gpgme @@ -573,67 +572,3 @@ def recieved_key(self, fingerprint, keydata, *data): self.received_key_data = keydata gpgmeKey = gpg.gpg_get_keylist(self.ctx, fingerprint, False)[0] self.signPage.display_downloaded_key(gpg.gpg_format_key(gpgmeKey)) - - - - -class SignUi(MonkeysignUi): - """sign a key in a safe fashion. - -This program assumes you have gpg-agent configured to prompt for -passwords.""" - - def __init__(self, app, args = None): - super(SignUi, self).__init__(args) - - self.app = app - - - def main(self): - - MonkeysignUi.main(self) - - def yes_no(self, prompt, default = None): - # dialog = Gtk.MessageDialog(self.app.window, 0, Gtk.MessageType.INFO, - # Gtk.ButtonsType.YES_NO, prompt) - # response = dialog.run() - # dialog.destroy() - - # return response == Gtk.ResponseType.YES - # Simply return True for now - return True - - def choose_uid(self, prompt, key): - # dialog = Gtk.Dialog(prompt, self.app.window, 0, - # (Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT, - # Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT)) - - # label = Gtk.Label(prompt) - # dialog.vbox.pack_start(label, False, False, 0) - # label.show() - - # self.uid_radios = None - # for uid in key.uidslist: - # r = Gtk.RadioButton.new_with_label_from_widget( - # self.uid_radios, uid.uid) - # r.show() - # dialog.vbox.pack_start(r, False, False, 0) - - # if self.uid_radios is None: - # self.uid_radios = r - # self.uid_radios.set_active(True) - # else: - # self.uid_radios.set_active(False) - - # response = dialog.run() - - # label = None - # if response == Gtk.ResponseType.ACCEPT: - # self.app.log.info("okay signing") - # label = [ r for r in self.uid_radios.get_group() if r.get_active()][0].get_label() - # else: - # self.app.log.info('user denied signature') - - # dialog.destroy() - # return label - return None From b4a779bf681378012b79c713314858a168b65652 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 3 Aug 2015 00:45:23 +0300 Subject: [PATCH 060/102] Updated RELEASE_NOTES and requirements files --- RELEASE_NOTES | 6 +++++- requirements.txt | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index e98f505..a9e7478 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -4,8 +4,12 @@ This is the v0.3 release and bring a few tiny fixes all over the program. Changes ========== +-Major + * New gpg library which replaces monkeysign dependency * Optimised finding the correct key to download * Removed once discovered clients if they disappear + +-Minor * Experimental compatibility for older Gtk versions (set_always_show_image) * Minor UI improvement: Do not delete the manually typed fingerprint when going back * Also accept more correctly typed fingerprint with a more relaxed verification routine @@ -17,5 +21,5 @@ Resources ========= Download: https://github.com/muelli/geysigning/releases/download/0.3/gnome-keysign-0.3.tar.gz -sha256sum: +sha256sum: Web site: https://wiki.gnome.org/GnomeKeysign diff --git a/requirements.txt b/requirements.txt index 4582567..5f16e41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ # Debian packages -monkeysign==1.1 +python-gi +python-avahi +python-gpgme qrencode + +# Libs: python-dev, libqrencode-dev, libffi-dev, libssl-dev From 6ceb5240340d902b245ad946983838dceeb5beed Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 3 Aug 2015 00:59:02 +0300 Subject: [PATCH 061/102] Updated copyrights --- keysign/GPGQRCode.py | 1 + keysign/KeyPresent.py | 1 + keysign/KeysPage.py | 1 + keysign/__init__.py | 5 +++-- keysign/gpg/gpg.py | 17 +++++++++++++++++ keysign/gpg/test.py | 18 +++++++++++++++++- 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/keysign/GPGQRCode.py b/keysign/GPGQRCode.py index 438fd54..756314a 100755 --- a/keysign/GPGQRCode.py +++ b/keysign/GPGQRCode.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # Copyright 2014 Tobias Mueller +# Copyright 2015 Andrei Macavei # # This file is part of GNOME Keysign. # diff --git a/keysign/KeyPresent.py b/keysign/KeyPresent.py index d8332d5..0add80a 100644 --- a/keysign/KeyPresent.py +++ b/keysign/KeyPresent.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # encoding: utf-8 # Copyright 2014 Tobias Mueller +# Copyright 2015 Andrei Macavei # # This file is part of GNOME Keysign. # diff --git a/keysign/KeysPage.py b/keysign/KeysPage.py index c69bf0e..91584b4 100644 --- a/keysign/KeysPage.py +++ b/keysign/KeysPage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 +# Copyright 2015 Andrei Macavei # Copyright 2014 Tobias Mueller # # This file is part of GNOME Keysign. diff --git a/keysign/__init__.py b/keysign/__init__.py index b4af0f8..380e068 100644 --- a/keysign/__init__.py +++ b/keysign/__init__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # Copyright 2014 Tobias Mueller +# Copyright 2015 Andrei Macavei # # This file is part of GNOME Keysign. # @@ -24,9 +25,9 @@ def main(): # can be imported without wanting to run it, e.g. setup.py # imports the __version__ import logging, sys, signal - + from gi.repository import GLib, Gtk - + from .MainWindow import MainWindow app = MainWindow() diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index d3206ac..19a4b21 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -1,4 +1,21 @@ #!/usr/bin/env python +# Copyright 2015 Andrei Macavei +# +# This file is part of GNOME Keysign. +# +# GNOME Keysign is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# GNOME Keysign is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNOME Keysign. If not, see . + import logging from string import Template diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index ac94c5e..930a7a6 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -1,4 +1,20 @@ -#!/bin/bash/env python +#!/usr/bin/env python +# Copyright 2015 Andrei Macavei +# +# This file is part of GNOME Keysign. +# +# GNOME Keysign is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# GNOME Keysign is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNOME Keysign. If not, see . import os import sys From aef766c0e0910aad88a5d4888b8bff0f91d206f4 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 12:44:28 +0300 Subject: [PATCH 062/102] gpg: Fixed ImportError for Python3 on StringIO --- keysign/gpg/gpg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 19a4b21..44ca947 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -28,7 +28,10 @@ import gpgme import gpgme.editutil from io import BytesIO -from StringIO import StringIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO log = logging.getLogger() From 49bed8b43c095de47f969e73cfb2f24e3519a55a Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 12:50:56 +0300 Subject: [PATCH 063/102] gpg: Added a '__name__' argument to logging.getLogger() --- keysign/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 44ca947..a71c81d 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -34,7 +34,7 @@ from io import StringIO -log = logging.getLogger() +log = logging.getLogger(__name__) gpg_default = os.environ['HOME'] + '/.gnupg/' gpg_path = find_executable('gpg') From b5073a95fa3368e440e21ffb0bcba6704c783c3f Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 14:09:27 +0300 Subject: [PATCH 064/102] gpg: Refactored gpg_import_key_by_fpr funtion to be more clear about what it does Also shortend its name to gpg_import_key. --- keysign/gpg/gpg.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a71c81d..c093d8f 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -83,25 +83,26 @@ def gpg_copy_secrets(gpgmeContext, gpg_homedir): for key in secret_keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: - gpg_import_key_by_fpr(gpgmeContext, key.subkeys[0].fpr) + gpg_import_key(gpgmeContext, key.subkeys[0].fpr) -def gpg_import_key_by_fpr(gpgmeContext, fpr): - """Imports a key received by its @fpr into a temporary keyring. +def gpg_import_key(gpgmeContext, fpr): + """Imports a key from the user's keyring into the keyring received + as argument. - It assumes that the received @gpgmeContext has its gpg homedir set already. + It assumes that the received keyring (@gpgmeContext) has its gpg homedir set already. """ - # We make a new context because we need to get the key from it + # Get the default keyring ctx = gpgme.Context() - keydata = extract_keydata(ctx, fpr, True) - # It seems that keys can be imported from string streams only - keydataIO = StringIO(keydata) - try: - res = gpgmeContext.import_(keydataIO) - except gpgme.GpgmeError as err: + ctx.armor = True + keydata = StringIO() + ctx.export(fpr, keydata) + + if not keydata.getvalue(): log.error("No key found in user's keyring with fpr:\n%s", fpr) raise ValueError('Invalid fingerprint') + res = gpgmeContext.import_(keydata) return len(res.imports) != 0 From 09ec3520a590467d51a8648f2f0d418b6449eb0c Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 15:04:06 +0300 Subject: [PATCH 065/102] gpg: raised ValueError now contains the logged string --- keysign/gpg/gpg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index c093d8f..3602eaa 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -162,8 +162,9 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): try: uid_name, uid_email, uid_comment = userId.name, userId.email, userId.comment except AttributeError as exp: - log.error("%s is not a valid gpgme.UserId", userId) - raise ValueError("Invalid UID") + msg = "Invalid UserId object: %s" % userId + log.error(msg) + raise ValueError(msg) # we set keylist mode so we can see signatures gpgmeContext.keylist_mode = gpgme.KEYLIST_MODE_SIGS From 21057f8cd7abe6c55d93c7569507ecde23440426 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 15:20:00 +0300 Subject: [PATCH 066/102] Renamed extract_keydata function and updated the docstrings --- keysign/Sections.py | 6 +++--- keysign/gpg/gpg.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 71b127c..b977377 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -147,7 +147,7 @@ def on_key_selected(self, pane, keyid): fpr = gpg.extract_fpr(self.ctx, keyid) - keydata = gpg.extract_keydata(self.ctx, fpr, True) + keydata = gpg.export_key(self.ctx, fpr, True) self.log.debug("Keyserver switched on! Serving key with fpr: %s", fpr) self.app.setup_server(keydata, fpr) @@ -398,7 +398,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): if not keydata: self.log.debug("looking for key %s in your keyring", fingerprint) default_ctx = gpgme.Context() - keydata = gpg.extract_keydata(default_ctx, fingerprint, True) + keydata = gpg.export_key(default_ctx, fingerprint, True) # 1. Fetch the key into a temporary keyring self.log.debug('Trying to import key\n%s', keydata) @@ -423,7 +423,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): continue # 3. Export and encrypt the signature - signed_key = gpg.extract_keydata(ctx, fingerprint, True) + signed_key = gpg.export_key(ctx, fingerprint, True) self.log.info("Exported %d bytes of signed key", len(signed_key)) encrypted_key = gpg.gpg_encrypt_data(ctx, signed_key, uid_str) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 3602eaa..8ce85e1 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -218,9 +218,10 @@ def extract_fpr(gpgmeContext, keyid): return key.subkeys[0].fpr -def extract_keydata(gpgmeContext, fpr, armor=False): - """Extracts key data from a key with fingerprint @fpr. - Returns the data in plaintext (if armor=True) or binary. +def export_key(gpgmeContext, fpr, armor=False): + """Exports the key with the given fingerprint from the user's keyring. + + The key can be exported in ASCII-armored format if armor is set. """ gpgmeContext.armor = armor keydata = StringIO() From 406cfe40d08d2a66fab159e4466b2be8bc45b324 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 4 Aug 2015 15:39:28 +0300 Subject: [PATCH 067/102] test_gpg_copy_secrets: made it to verify keys in a more pythonic way --- keysign/gpg/test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index 930a7a6..4a90329 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -86,10 +86,9 @@ def test_gpg_copy_secrets(self): secret_keys = [key for key in ctx.keylist(None, True)] self.assertEqual(len(secret_keys), len(default_secret_keys)) - i = 0 - for i in xrange(len(secret_keys)): - self.assertEqual(secret_keys[i].subkeys[0].fpr, default_secret_keys[i].subkeys[0].fpr) - i += 1 + all_keys = sum(key1.subkeys[0].fpr != key2.subkeys[0].fpr for key1, key2 in zip(secret_keys, default_secret_keys)) + self.assertFalse(all_keys) + # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) From 79b00801da6aac0953b1242b39fd08fb14a22f05 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 8 Aug 2015 10:52:13 +0200 Subject: [PATCH 068/102] test.py: fixed function name and removed obsolete test --- keysign/gpg/test.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index 4a90329..f8ac3e6 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -92,22 +92,6 @@ def test_gpg_copy_secrets(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_import_key_by_fpr(self): - ctx = gpgme.Context() - tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) - - with self.keyfile('testkey1.pub') as fp: - ctx.import_(fp) - - res = gpg.gpg_import_key_by_fpr(ctx, '31E91E906BA25D74BB315DEA9B33CFC7BB70DAFA') - self.assertTrue(res) - - # can we get the key ? - key = ctx.get_key('31E91E906BA25D74BB315DEA9B33CFC7BB70DAFA') - # clean temporary dir - shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_import_keydata(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') From 43614eaacf5c4b89deb7a8a149a0e8558c24e8ba Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 9 Aug 2015 13:16:36 +0200 Subject: [PATCH 069/102] gpg: fixed gpg_import_key and switched to BytesIO instead of StringIO for keydata --- keysign/gpg/gpg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 8ce85e1..1a21cff 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -94,14 +94,14 @@ def gpg_import_key(gpgmeContext, fpr): """ # Get the default keyring ctx = gpgme.Context() - ctx.armor = True - keydata = StringIO() + keydata = BytesIO() ctx.export(fpr, keydata) if not keydata.getvalue(): log.error("No key found in user's keyring with fpr:\n%s", fpr) raise ValueError('Invalid fingerprint') + keydata.seek(0) res = gpgmeContext.import_(keydata) return len(res.imports) != 0 @@ -114,7 +114,7 @@ def gpg_import_keydata(gpgmeContext, keydata): # XXX: PyGPGME key imports doesn't work with data as unicode strings # but here we get data coming from network which is unicode keydata = keydata.encode('utf-8') - keydataIO = StringIO(keydata) + keydataIO = BytesIO(keydata) try: result = gpgmeContext.import_(keydataIO) except gpgme.GpgmeError as err: @@ -162,7 +162,7 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): try: uid_name, uid_email, uid_comment = userId.name, userId.email, userId.comment except AttributeError as exp: - msg = "Invalid UserId object: %s" % userId + msg = "Invalid UserId object: %r" % userId log.error(msg) raise ValueError(msg) @@ -224,7 +224,7 @@ def export_key(gpgmeContext, fpr, armor=False): The key can be exported in ASCII-armored format if armor is set. """ gpgmeContext.armor = armor - keydata = StringIO() + keydata = BytesIO() gpgmeContext.export(fpr, keydata) return keydata.getvalue() From 32f86130adfdd2c02eaa89d10017fa3842afc350 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 9 Aug 2015 13:40:56 +0200 Subject: [PATCH 070/102] gpg: small improvement on getting the default gpg home --- keysign/gpg/gpg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 1a21cff..ea8bdf3 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -36,7 +36,7 @@ log = logging.getLogger(__name__) -gpg_default = os.environ['HOME'] + '/.gnupg/' +gpg_default = os.environ.get('GNUPGHOME', os.environ['HOME'] + '/.gnupg/') gpg_path = find_executable('gpg') From 45ba7bfdb580c2efed53fc10c4bb183b20022971 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 16 Aug 2015 16:06:26 +0300 Subject: [PATCH 071/102] export_key: added support for exporting key with additional 'mode' argument In Sections.py I've added the link to the patch for pygpgme which adds support for exporting with 'EXPORT_MODE_MINIMAL' and 'EXPORT_MODE_EXTERN' options. Until this gets merged upstream, you can always merge it into your local pygpgme by using "bzr merge lp:~daniele-athome/pygpgme/pygpgme" on your own copy of pygpgme. --- keysign/Sections.py | 6 +++--- keysign/gpg/gpg.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index b977377..d24674e 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -392,9 +392,9 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): gpg_homedir = gpg.gpg_set_engine(ctx) keydata = data or self.received_key_data - # FIXME: until we have our patch to PyGPGME integrated, we cannot - # export the key with 'export-minimal' option - # stripped_key = gpg.gpg_export_minimal(ctx, keydata) + # FIXME: until this (https://code.launchpad.net/~daniele-athome/pygpgme/pygpgme/+merge/173333) + # gets merged in trunk, we cannot export the key with 'export-minimal' option + # stripped_key = gpg.gpg_export(ctx, keydata, True, gpgme.EXPORT_MODE_MINIMAL) if not keydata: self.log.debug("looking for key %s in your keyring", fingerprint) default_ctx = gpgme.Context() diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index ea8bdf3..d64fd18 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -218,14 +218,17 @@ def extract_fpr(gpgmeContext, keyid): return key.subkeys[0].fpr -def export_key(gpgmeContext, fpr, armor=False): +def export_key(gpgmeContext, fpr, armor=False, mode=None): """Exports the key with the given fingerprint from the user's keyring. The key can be exported in ASCII-armored format if armor is set. """ gpgmeContext.armor = armor keydata = BytesIO() - gpgmeContext.export(fpr, keydata) + if mode: + gpgmeContext.export(fpr, keydata, mode) + else: + gpgmeContext.export(fpr, keydata) return keydata.getvalue() From bd6ff0e211286f734a3cba8675a5790279a64a66 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 16 Aug 2015 16:31:41 +0300 Subject: [PATCH 072/102] gpg: Contexts can be set easier, w/o having to specify user's default paths The Context.set_engine_info() method can take None as second and third args, which means the default 'gpg executable path' and default 'gnupg homedir'. --- keysign/gpg/gpg.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index d64fd18..9fa5ee9 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -23,7 +23,6 @@ import os import shutil import tempfile -from distutils.spawn import find_executable import gpgme import gpgme.editutil @@ -36,25 +35,20 @@ log = logging.getLogger(__name__) -gpg_default = os.environ.get('GNUPGHOME', os.environ['HOME'] + '/.gnupg/') -gpg_path = find_executable('gpg') - - def gpg_set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=None): """Sets up a temporary directory as new gnupg home for this context """ dir_prefix = dir_prefix if dir_prefix else 'tmp.gpghome' temp_dir = tempfile.mkdtemp(prefix=dir_prefix) - gpgmeContext.set_engine_info(protocol, gpg_path, temp_dir) + gpgmeContext.set_engine_info(protocol, None, temp_dir) return temp_dir def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP): - """Resets the gnupg dir to its default location - for current context + """Resets the gnupg homedir for the received context """ - gpgmeContext.set_engine_info(protocol, gpg_path, gpg_default) + gpgmeContext.set_engine_info(protocol, None, None) if tmp_dir: shutil.rmtree(tmp_dir, ignore_errors=True) @@ -63,7 +57,7 @@ def gpg_copy_secrets(gpgmeContext, gpg_homedir): """Copies secrets from .gnupg to new @gpg_homedir """ ctx = gpgme.Context() - + gpg_default = os.environ.get('GNUPGHOME', os.environ['HOME'] + '/.gnupg/') secring_path = gpg_default + 'secring.gpg' shutil.copy(secring_path, gpg_homedir) log.debug('copied your secring.gpg from %s to %s', secring_path, gpg_homedir) @@ -79,7 +73,7 @@ def gpg_copy_secrets(gpgmeContext, gpg_homedir): secret_keys = [key for key in ctx.keylist(None, True)] # We set again the gpg homedir because there is no contex method "get_engine_info" # to tell us what gpg home it uses. - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg_path, gpg_homedir) + gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, gpg_homedir) for key in secret_keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: From 11733d6f56989243a42719c6c7dfc02ea4fbea05 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 15:58:51 +0300 Subject: [PATCH 073/102] gpg_copy_secrets: gpg_default path is now more robust Also added comment with link to GPGME bug report about not being able to export with 'export-minimal' options in GPGME. --- keysign/gpg/gpg.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 9fa5ee9..089d76e 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -56,9 +56,12 @@ def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP def gpg_copy_secrets(gpgmeContext, gpg_homedir): """Copies secrets from .gnupg to new @gpg_homedir """ + # XXX: Latest report about not being able to export private key in GPGME can be found here + # https://lists.gnupg.org/pipermail/gnupg-devel/2015-August/030229.html + # Until then, we will use this hack to import user's private key into a temp keyring ctx = gpgme.Context() - gpg_default = os.environ.get('GNUPGHOME', os.environ['HOME'] + '/.gnupg/') - secring_path = gpg_default + 'secring.gpg' + gpg_default = os.environ.get('GNUPGHOME', os.path.join(os.environ['HOME'], '.gnupg')) + secring_path = os.path.join(gpg_default, 'secring.gpg') shutil.copy(secring_path, gpg_homedir) log.debug('copied your secring.gpg from %s to %s', secring_path, gpg_homedir) From 3bc8ed83320c8dc111169200edeb983b2a8a924b Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 16:35:07 +0300 Subject: [PATCH 074/102] Fixed tests which were broken by latest code updates. --- keysign/gpg/test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index f8ac3e6..94a9313 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -32,7 +32,7 @@ __all__ = ['GpgTestSuite'] keydir = os.path.join(os.path.dirname(__file__), 'keys') -gpg_default = os.environ['HOME'] + '/.gnupg/' +gpg_default = os.environ.get('GNUPGHOME', os.path.join(os.environ['HOME'], '.gnupg')) class GpgTestSuite(unittest.TestCase): @@ -58,7 +58,7 @@ def test_gpg_reset_engine(self): ctx = gpgme.Context() # set a temporary dir for gpg home tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) # check if we have created the new gpg dir self.assertTrue(os.path.isdir(tmpdir)) @@ -73,13 +73,13 @@ def test_gpg_reset_engine(self): def test_gpg_copy_secrets(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) gpg.gpg_copy_secrets(ctx, tmpdir) # get the user's secret keys default_ctx = gpgme.Context() - default_ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, gpg_default) + default_ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, gpg_default) default_secret_keys = [key for key in default_ctx.keylist(None, True)] # compare the imported keys with the original secret keys @@ -95,7 +95,7 @@ def test_gpg_copy_secrets(self): def test_gpg_import_keydata(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) with self.keyfile('testkey1.pub') as fp: keydata = fp.read() @@ -109,7 +109,7 @@ def test_gpg_import_keydata(self): def test_gpg_sign_uid(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, gpg.gpg_path, tmpdir) + ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) with self.keyfile('testkey1.pub') as fp: ctx.import_(fp) From 203579305792df923427ae35291037938602143d Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 17:22:58 +0300 Subject: [PATCH 075/102] 'gpg_import_keydata' will now return the results object generated by import --- keysign/Sections.py | 5 +++-- keysign/gpg/gpg.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index d24674e..3df9bb7 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -319,8 +319,9 @@ def try_download_keys(self, clients): def verify_downloaded_key(self, downloaded_data, fingerprint): # FIXME: implement a better and more secure way to verify the key - if gpg.gpg_import_keydata(self.ctx, downloaded_data): - imported_key_fpr = gpg.gpg_get_keylist(self.ctx, None, False)[0].subkeys[0].fpr + res = gpg.gpg_import_keydata(self.ctx, downloaded_data) + if res and len(res.imports): + (imported_key_fpr, null, null) = res.imports[0] if imported_key_fpr == fingerprint: result = True else: diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 089d76e..b989b0f 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -112,14 +112,12 @@ def gpg_import_keydata(gpgmeContext, keydata): # but here we get data coming from network which is unicode keydata = keydata.encode('utf-8') keydataIO = BytesIO(keydata) + result = None try: result = gpgmeContext.import_(keydataIO) except gpgme.GpgmeError as err: log.error("Couldn't import the key with the following keydata:\n%s", keydataIO.getvalue()) - return False - # XXX: we stick to return True/False for compatibility issues. - # The gpgme.ImportResult can be used to extract more information. - return True + return result def gpg_get_keylist(gpgmeContext, keyid=None, secret=False): From 3e1ad2b033f11d72c5db905b4903d303a1f98844 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 19:05:19 +0300 Subject: [PATCH 076/102] Code refactoring to make 'gpg_sign_uid' more robust. --- keysign/gpg/gpg.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index b989b0f..40a4f7b 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -165,16 +165,16 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): gpgmeContext.keylist_mode = gpgme.KEYLIST_MODE_SIGS key = gpgmeContext.get_key(userId.uid) - i = 1 # check if we didn't already signed this uid of the key - for uid in key.uids: + for (i, uid) in enumerate(key.uids): + # Get all signature of our primary key on this uid sigs = [sig for sig in uid.signatures if primary_key.subkeys[0].fpr.endswith(sig.keyid)] # check if this uid is the same with the one that we want to sign - if (uid.name.startswith(uid_name) and uid.email.startswith(uid_email) - and uid.comment.startswith(uid_comment)): + if uid.name == uid_name and uid.email == uid_email and uid.comment == uid_comment: if len(sigs) == 0: + # if we haven't signed it yet gpgme.editutil.edit_sign(gpgmeContext, key, index=i, check=0) else: @@ -182,7 +182,6 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): log.info("Uid %s was already signed by key: \n%s", userId.uid, key.subkeys[0].fpr) return False break - i += 1 return True From 88ddd4da44896f4c887165249a0b2c1916ea1451 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 20:04:57 +0300 Subject: [PATCH 077/102] Improved the way we get the selected key fpr from within the UI. This also makes obsolete the gpg lib "extract_fpr". --- keysign/Sections.py | 3 ++- keysign/gpg/gpg.py | 13 ------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 3df9bb7..85381ac 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -145,7 +145,8 @@ def on_key_selected(self, pane, keyid): ''' self.log.info('User selected key %s', keyid) - fpr = gpg.extract_fpr(self.ctx, keyid) + key = self.keysPage.keysDict[keyid] + fpr = key.subkeys[0].fpr keydata = gpg.export_key(self.ctx, fpr, True) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 40a4f7b..bc72f5e 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -199,19 +199,6 @@ def gpg_encrypt_data(gpgmeContext, data, uid, armor=True): return ciphertext.getvalue() -def extract_fpr(gpgmeContext, keyid): - """Extracts the fingerprint of a key with @keyid. - """ - try: - key = gpgmeContext.get_key(keyid) - except gpgme.GpgmeError as err: - log.error('No key found with id: %s', keyid) - raise ValueError("Invalid keyid") - - # A gpgme.Key object doesn't have a fpr but a gpgme.Subkey does - return key.subkeys[0].fpr - - def export_key(gpgmeContext, fpr, armor=False, mode=None): """Exports the key with the given fingerprint from the user's keyring. From b16133abe45fc655ad6326b5c633ba087c5a4548 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 17 Aug 2015 20:39:41 +0300 Subject: [PATCH 078/102] Updated 'gpg_sign_uid' to sign also the uids that were already signed. --- keysign/Sections.py | 4 ++-- keysign/gpg/gpg.py | 26 +++++++++++--------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 85381ac..b9a0ed2 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -421,8 +421,8 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): res = gpg.gpg_sign_uid(ctx, gpg_homedir, uid) if not res: # we may have already signed this uid before - self.log.info("Uid %s couldn't be signed. Maybe we already signed it?", uid_str) - continue + self.log.info("Uid %s was signed before.\nUpdating signature made by key: %s", + uid_str, key.subkeys[0].fpr) # 3. Export and encrypt the signature signed_key = gpg.export_key(ctx, fingerprint, True) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index bc72f5e..9bf125d 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -149,7 +149,7 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): @gpg_homedir is the directory that @gpgmeContext uses for gpg. @userId is a gpgme.UserId object """ - # we import the user's primary key that will be used to sign + # We import the user's primary key that will be used to sign gpg_copy_secrets(gpgmeContext, gpg_homedir) primary_key = [key for key in gpgmeContext.keylist(None, True)][0] gpgmeContext.signers = [primary_key] @@ -161,29 +161,25 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): log.error(msg) raise ValueError(msg) - # we set keylist mode so we can see signatures + # We set keylist mode so we can see signatures gpgmeContext.keylist_mode = gpgme.KEYLIST_MODE_SIGS key = gpgmeContext.get_key(userId.uid) - # check if we didn't already signed this uid of the key - for (i, uid) in enumerate(key.uids): - # Get all signature of our primary key on this uid - sigs = [sig for sig in uid.signatures if primary_key.subkeys[0].fpr.endswith(sig.keyid)] + first_sig = True - # check if this uid is the same with the one that we want to sign + for (i, uid) in enumerate(key.uids): + # Check if this uid is the same with the one that we want to sign if uid.name == uid_name and uid.email == uid_email and uid.comment == uid_comment: - if len(sigs) == 0: - # if we haven't signed it yet - gpgme.editutil.edit_sign(gpgmeContext, key, index=i, check=0) + sigs = [sig for sig in uid.signatures if primary_key.subkeys[0].fpr.endswith(sig.keyid)] + if not len(sigs) == 0: + # This uid was signed by this key in the past. Update signature but set 'first_sig' to False + first_sig = False - else: - # we already signed this UID - log.info("Uid %s was already signed by key: \n%s", userId.uid, key.subkeys[0].fpr) - return False + gpgme.editutil.edit_sign(gpgmeContext, key, index=i, check=0) break - return True + return first_sig def gpg_encrypt_data(gpgmeContext, data, uid, armor=True): From 4bef2db7b2b03d46b702610064298baa482c1960 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 20 Aug 2015 14:58:02 +0300 Subject: [PATCH 079/102] Changed gpg_copy_secrets to gpg_export_private_key. This also changes the behavior of the function, now it will export the user's private key into a temporary file which will be immediately deleted after we import from the key into a temporary keyring. In this way we work in a temporar directory so we don't import and sign keys inside user's default keyring. While people don't like this way of exporting private key, we lack support from GPGME which doesn't offer this option. --- keysign/gpg/gpg.py | 46 ++++++++++++++++++++++++--------------------- keysign/gpg/test.py | 7 ++++--- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 9bf125d..a27a39c 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -21,8 +21,10 @@ from string import Template import os +from os.path import expanduser import shutil import tempfile +import subprocess import gpgme import gpgme.editutil @@ -53,35 +55,37 @@ def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP shutil.rmtree(tmp_dir, ignore_errors=True) -def gpg_copy_secrets(gpgmeContext, gpg_homedir): - """Copies secrets from .gnupg to new @gpg_homedir +def gpg_export_private_key(gpgmeContext, gpg_homedir): + """Exports user's private key from default keyring to a temporary file. + + Then it imports the private key back into a temporary keyring and deletes + the temporary file. """ # XXX: Latest report about not being able to export private key in GPGME can be found here # https://lists.gnupg.org/pipermail/gnupg-devel/2015-August/030229.html # Until then, we will use this hack to import user's private key into a temp keyring + + # Set up a tmp dir for exporting the private key + tmp_dir = tempfile.mkdtemp(prefix="tmp.gpgsecret", dir=".") + key_file = os.path.join(tmp_dir, 'secret-keys.gpg') + subprocess.call(["gpg", "--export-secret-keys", "--output", key_file]) + log.debug("exported your private key to: %s", key_file) + + with open(key_file, 'rb') as fp: + gpgmeContext.import_(fp) + + # Import the public key part for the private keys ctx = gpgme.Context() - gpg_default = os.environ.get('GNUPGHOME', os.path.join(os.environ['HOME'], '.gnupg')) - secring_path = os.path.join(gpg_default, 'secring.gpg') - shutil.copy(secring_path, gpg_homedir) - log.debug('copied your secring.gpg from %s to %s', secring_path, gpg_homedir) + keys = [key for key in ctx.keylist(None, True)] - try: - conf_path = gpg_default + 'gpg.conf' - shutil.copy(conf_path, gpg_homedir) - log.debug('copied your gpg.conf from %s to %s', conf_path, gpg_homedir) - except IOError as e: - log.error('User has no gpg.conf file') - - # Imports user's private keys into the new keyring - secret_keys = [key for key in ctx.keylist(None, True)] - # We set again the gpg homedir because there is no contex method "get_engine_info" - # to tell us what gpg home it uses. - gpgmeContext.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, gpg_homedir) - - for key in secret_keys: + for key in keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: gpg_import_key(gpgmeContext, key.subkeys[0].fpr) + # Delete the tmp dir along with the secret key file + shutil.rmtree(tmp_dir, ignore_errors=True) + log.debug("removed temporary secret key directory: %s", tmp_dir) + def gpg_import_key(gpgmeContext, fpr): """Imports a key from the user's keyring into the keyring received @@ -150,7 +154,7 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): @userId is a gpgme.UserId object """ # We import the user's primary key that will be used to sign - gpg_copy_secrets(gpgmeContext, gpg_homedir) + gpg_export_private_key(gpgmeContext, gpg_homedir) primary_key = [key for key in gpgmeContext.keylist(None, True)][0] gpgmeContext.signers = [primary_key] diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index 94a9313..47ea95b 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -17,6 +17,7 @@ # along with GNOME Keysign. If not, see . import os +from os.path import expanduser import sys import shutil import tempfile @@ -32,7 +33,7 @@ __all__ = ['GpgTestSuite'] keydir = os.path.join(os.path.dirname(__file__), 'keys') -gpg_default = os.environ.get('GNUPGHOME', os.path.join(os.environ['HOME'], '.gnupg')) +gpg_default = os.environ.get('GNUPGHOME', os.path.join(expanduser("~"), '.gnupg')) class GpgTestSuite(unittest.TestCase): @@ -70,12 +71,12 @@ def test_gpg_reset_engine(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_copy_secrets(self): + def test_gpg_export_private_key(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) - gpg.gpg_copy_secrets(ctx, tmpdir) + gpg.gpg_export_private_key(ctx, tmpdir) # get the user's secret keys default_ctx = gpgme.Context() From 1ae536a3da6e9005869a88ef6ee7298630d876f5 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 20 Aug 2015 18:08:51 +0300 Subject: [PATCH 080/102] Eliminated a possible filesystem leak by exporting the private key to stdout Changed the function's name to reflect more what it does, it imports the user's private key into a temporary context. It can also receive a secret_key as argument now and it will import from that instead of user's default keyring. Removed extra argument 'gpg_homedir' as it only was used with logging. --- keysign/gpg/gpg.py | 36 +++++++++++++++--------------------- keysign/gpg/test.py | 4 ++-- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a27a39c..a262e33 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -21,7 +21,7 @@ from string import Template import os -from os.path import expanduser +import sys import shutil import tempfile import subprocess @@ -55,36 +55,30 @@ def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP shutil.rmtree(tmp_dir, ignore_errors=True) -def gpg_export_private_key(gpgmeContext, gpg_homedir): - """Exports user's private key from default keyring to a temporary file. - - Then it imports the private key back into a temporary keyring and deletes - the temporary file. +def gpg_import_private_key(gpgmeContext, secret_key=None): + """Imports the user's private key from @secret_key or the default keyring + to a temporary context. """ - # XXX: Latest report about not being able to export private key in GPGME can be found here - # https://lists.gnupg.org/pipermail/gnupg-devel/2015-August/030229.html - # Until then, we will use this hack to import user's private key into a temp keyring - - # Set up a tmp dir for exporting the private key - tmp_dir = tempfile.mkdtemp(prefix="tmp.gpgsecret", dir=".") - key_file = os.path.join(tmp_dir, 'secret-keys.gpg') - subprocess.call(["gpg", "--export-secret-keys", "--output", key_file]) - log.debug("exported your private key to: %s", key_file) + if secret_key: + keydata = secret_key + else: + # XXX: There is no option to export a private key in GPGME. Latest post about it can + # be found here: https://lists.gnupg.org/pipermail/gnupg-devel/2015-August/030229.html + # We use this hack for now to import user's private key into a temp keyring + keydata = subprocess.check_output(["gpg", "--armor", "--export-secret-keys"]) - with open(key_file, 'rb') as fp: + with BytesIO(keydata) as fp: gpgmeContext.import_(fp) - # Import the public key part for the private keys + # Import the personal public keys also ctx = gpgme.Context() keys = [key for key in ctx.keylist(None, True)] for key in keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: gpg_import_key(gpgmeContext, key.subkeys[0].fpr) + log.debug("imported your personal key: %s to tmp keyring", key.subkeys[0].fpr) - # Delete the tmp dir along with the secret key file - shutil.rmtree(tmp_dir, ignore_errors=True) - log.debug("removed temporary secret key directory: %s", tmp_dir) def gpg_import_key(gpgmeContext, fpr): @@ -154,7 +148,7 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): @userId is a gpgme.UserId object """ # We import the user's primary key that will be used to sign - gpg_export_private_key(gpgmeContext, gpg_homedir) + gpg_import_private_key(gpgmeContext) primary_key = [key for key in gpgmeContext.keylist(None, True)][0] gpgmeContext.signers = [primary_key] diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index 47ea95b..dcbe3ec 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -71,12 +71,12 @@ def test_gpg_reset_engine(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_export_private_key(self): + def test_gpg_import_private_key(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) - gpg.gpg_export_private_key(ctx, tmpdir) + gpg.gpg_import_private_key(ctx) # get the user's secret keys default_ctx = gpgme.Context() From f99bd8697025cb8fc449b605714958824ec77e67 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 20 Aug 2015 23:50:09 +0300 Subject: [PATCH 081/102] Added a 'secret_key' argument to 'gpg_sign_uid' function. The function can now receive a secret key passed as argument, to use as a signer key instead of the user's primary key. Added a 'sign only' DSA key to be used as test signer key in tests. --- keysign/gpg/gpg.py | 16 +++++++--- keysign/gpg/keys/signonly.pub | 32 +++++++++++++++++++ keysign/gpg/keys/signonly.sec | 18 +++++++++++ keysign/gpg/keys/testkey1.sec | 59 +++++++++++++++++++++++++++++++++++ keysign/gpg/keys/testkey2.sec | 35 +++++++++++++++++++++ keysign/gpg/test.py | 6 +++- 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 keysign/gpg/keys/signonly.pub create mode 100644 keysign/gpg/keys/signonly.sec create mode 100644 keysign/gpg/keys/testkey1.sec create mode 100644 keysign/gpg/keys/testkey2.sec diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index a262e33..5e520c7 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -141,14 +141,20 @@ def gpg_get_siglist(gpgmeContext, keyid): return list(siglist) -def gpg_sign_uid(gpgmeContext, gpg_homedir, userId): +def gpg_sign_uid(gpgmeContext, gpg_homedir, userId, secret_key=None): """Signs a specific uid of a OpenPGP key - @gpg_homedir is the directory that @gpgmeContext uses for gpg. - @userId is a gpgme.UserId object + gpgmeContext: the temporary keyring + gpg_homedir: the current GPG directory + userId: sign this userId + secret_key: if this is given than it will be used as the primary key + + @rtype: bool + @return: True/False depending if this uid was signed for the first time + by this key """ - # We import the user's primary key that will be used to sign - gpg_import_private_key(gpgmeContext) + gpg_import_private_key(gpgmeContext, secret_key) + primary_key = [key for key in gpgmeContext.keylist(None, True)][0] gpgmeContext.signers = [primary_key] diff --git a/keysign/gpg/keys/signonly.pub b/keysign/gpg/keys/signonly.pub new file mode 100644 index 0000000..9d12dab --- /dev/null +++ b/keysign/gpg/keys/signonly.pub @@ -0,0 +1,32 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mI0EVbqCqAEEAKYNDbu4zCTOtOb8tnODaA7WnBHM8rSFFR4Hyd+S6LYMKY3n89cn +sacrnsuYaOH8c+cQlLNBAI9jzerwm3WhV0xc/muUb3yfcUzTXxCXpKfizwms4dIY +dPmLID1MUjqORaBhKIyyzgRlPJoIzp7i9NVrAhq7TdhW73uk7lFFLkedABEBAAG0 +K0pvaG55IERvZSAoVGVzdCBrZXkgMikgPGpvaG55LmRvZUB0ZXN0LmNvbT6IvgQT +AQIAKAUCVbqCqAIbAwUJACeNAAYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQ +8ef033cVdY7PIAP+PUfTAdjyklsycxJYWPcfUQi/YS/GfhgPk5k02ktoCyYZD+ar +bOHvNxkYAXQwzJomx/XAWVcW2iFD5E/ObqmKP+lEf8Ef1/FRIWJja9qIZkRYZrkx +8DkHp7fcpZWI1MWBRd6DPbYFp4Pz4dCQKi+hO/EsQIMuUYn9o1ZJPxOUXgW4jQRV +uoKoAQQAtswckAKtMTz8MWZiD+iCPbiSLF3cWaTv3ZklB8952Nmzx7sfx1Kj7uzL +OyGnBH8p0USFIdpXLZqa0io1cZGms9hvHz0RybXQDu3Id363s6OrJY9XnGxyMwle +OTYHL9L6bkRmXbozkzjO22nZztJuviZywzTEASt7VcDpy3XwIXUAEQEAAYilBBgB +AgAPBQJVuoKoAhsMBQkAJ40AAAoJEPHn9N93FXWOuw0D/3kx4rNf5BDocA96L4kP +T9YrVgSs7N71wRPWxFrO2GCVP9K4jaflFxOZp93Me/wvJ7EcHB+R7LGFcaP29WfJ +h4bDszKoZFm40Wd9Uu7y72jXJdvp+oSRU8pFyBz4ZVRSSDJ6aVs4LeYihV0EOej3 +5syUccEA5wa96mTyAsj4/SzNmQGiBFXWOhkRBAChNwrx1m51/bQ1Bllu1vl3h73A +VBmZ9rl8kGbfOUw+PKUPmaTw9SvdIKj6U1sZC4XSTUL5Cj8C1Sgf6RfW2cFEw9NB +R9ux14/y3PfM8odxbdpcSenm1+Ak3SSysMNUHv6aDy49t+grLRrldcKEzaR9RPWJ +3mE+OTSU07SxBYNq7wCgoB5dEiK2gpKr3LcwlRQFoszDks8D/1jQA8cXnDAT0k5R +29OtsJauvnIf+qmnp/FD80sBllpLjVBU097fqBs5mlkmNay/s9Lv52LqliqCmUwT +A8KeitYVLq3P1qiGi+qIAMKwSbAZjZ35G9cMv6uAzXU3dCCwjluoyBY9hJFlEeOd +Oyq2C409dFH7DYUBFbGIIx5EfqTQA/9mVDd1XmHNuHkiIqsjPsCtfLPqCMiVqgkv +OtqewJgDlz46CRbPvMB3yr6mfumU7SRYJkkBTDov8fcWue+HRRVaPza/bWsQLHpr +rXlPr0F06mcA2l/j5f2IVzP9L/cQ1uwTOjD9McdaI7du9SIr75jjmymIk1vWbZ46 +I7RLeCIgz7QrSm9obnkgRG9lIChUZXN0IEtleSAyKSA8am9obnkuZG9lQHRlc3Qu +Y29tPohoBBMRAgAoBQJV1joZAhsDBQkAJ40ABgsJCAcDAgYVCAIJCgsEFgIDAQIe +AQIXgAAKCRA783LT0y38+z2fAKCYEhEuSP5pAzetik+c8eK1WuQsfwCeLIw20S+9 +YSYTpTn+D3ai48AnqRQ= +=n3ZW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keysign/gpg/keys/signonly.sec b/keysign/gpg/keys/signonly.sec new file mode 100644 index 0000000..2225bbc --- /dev/null +++ b/keysign/gpg/keys/signonly.sec @@ -0,0 +1,18 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQG7BFXWOhkRBAChNwrx1m51/bQ1Bllu1vl3h73AVBmZ9rl8kGbfOUw+PKUPmaTw +9SvdIKj6U1sZC4XSTUL5Cj8C1Sgf6RfW2cFEw9NBR9ux14/y3PfM8odxbdpcSenm +1+Ak3SSysMNUHv6aDy49t+grLRrldcKEzaR9RPWJ3mE+OTSU07SxBYNq7wCgoB5d +EiK2gpKr3LcwlRQFoszDks8D/1jQA8cXnDAT0k5R29OtsJauvnIf+qmnp/FD80sB +llpLjVBU097fqBs5mlkmNay/s9Lv52LqliqCmUwTA8KeitYVLq3P1qiGi+qIAMKw +SbAZjZ35G9cMv6uAzXU3dCCwjluoyBY9hJFlEeOdOyq2C409dFH7DYUBFbGIIx5E +fqTQA/9mVDd1XmHNuHkiIqsjPsCtfLPqCMiVqgkvOtqewJgDlz46CRbPvMB3yr6m +fumU7SRYJkkBTDov8fcWue+HRRVaPza/bWsQLHprrXlPr0F06mcA2l/j5f2IVzP9 +L/cQ1uwTOjD9McdaI7du9SIr75jjmymIk1vWbZ46I7RLeCIgzwAAnip1Qeh0f1r0 +v9jhMBt0zEJdjcZtCwm0K0pvaG55IERvZSAoVGVzdCBLZXkgMikgPGpvaG55LmRv +ZUB0ZXN0LmNvbT6IaAQTEQIAKAUCVdY6GQIbAwUJACeNAAYLCQgHAwIGFQgCCQoL +BBYCAwECHgECF4AACgkQO/Ny09Mt/Ps9nwCgmBIRLkj+aQM3rYpPnPHitVrkLH8A +niyMNtEvvWEmE6U5/g92ouPAJ6kU +=ZRn8 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/keysign/gpg/keys/testkey1.sec b/keysign/gpg/keys/testkey1.sec new file mode 100644 index 0000000..6d5bd67 --- /dev/null +++ b/keysign/gpg/keys/testkey1.sec @@ -0,0 +1,59 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQO+BFW6eXgBCADY8dYFEmz1hh4sjMNZ8bC5jjJaVE0pv34ATX0QJdaiLJZCzZeY ++Ly5Sndxi0Jx0DlmkXGQrDuPNBjJPr4wS5ea5CZG2VD8flq+GkgskawPRAQQ4xTs +Yrgoo7AwTJVK2E05c9/rdV+ksHXray2XJc1XsQ0gF4ZBbBTHXGzrj1jXxvuGNwm1 +AP7fSlQcBC90inSJVsMduYUFRGSihkTcSrENP7kYBGqL8DjQW2lzQdrtE1SFQtdR +uRyuvznGExu/UkuspAZTft235mYmi2VQNsYmZ3qc7vZ7IiyXsF+ixgtS4fzCgyFA +ZoQe5kFt9vX9/u/SFRJSB9LBeP/OVFI5jZoDABEBAAH+AwMCeKDIRr3PFZBg89GP +giwhOm1sOoA0KO1atINCF3sPoMG+J4LxhO7vnGNOMPuQIa0fM4UHwcL4pS6SOxL8 +lQzMtx7/PzZOofKrbqfvVdPSMdPhn6RbaL3acj7ovVGJ1zYDcdXGb6MYvV9a6NLv +kfxHHFno16EJHN4drzu9sfdrJQy0yTXKfusMl8HD0SLusTT5sVc5aUDdxppuQX5e +OIGjBAvPtabXExJq8aSbjxkspbCSgFlK3r3FD1yvqXdrXNx3OiokdnCTnpS5ZRC2 +CNc/mrQWmL3uti5jHB/hYeFCx3a3sL2xSn5JRAvUrRVZ9PRL/zx9uKgcHSlb6p2p +BPe91A4AdEv2tSTt2SV6Og7uyUqegMRdlJ2/Rsj2ieO22msUsM/3RNUSpctxmJ6K +30QbVondU66vRaPhSFrP+5bZULZrau30RTlJWcxKfv809bxZ2DKFX3LhPJjx2CWf +tCK2AdVc7wAWET1TmuHqpwE4QySXTgcM4LcGqS1XAXqo1owUKxzSoRLTn7rYxxlI +fjiCztLl9GghHkJrDum77NYeGSGPuiF/iTeU/TCkV+1JpSkWatNW4pg+ozjgOXgh +U3Xk40oKHjcu9P8/R3+FHsMKg7ibqTkU2F2s/B3mxpMeFrR533VQE2wftbdLp4jU +7ktfrhfjkguUV/WX1vsYqqDunDilE+GdZCeGzXIKJ9YX1ne5ho95YiXCuMXQ5JTP +f0qo9DrgJiLUAZvaAabtxiwB+axfAYnpYr7L5mWayomXff7Qup5zoTF+wnlUOgLZ +2eNugCI3cRMKlcKDzTkCNFKR6HAmdqc9KegRMQWu/nTpA6Tj73YszdRvHWlUi4Ig +8qDkyENP2pX2oyL9xdh68fuYSlH3alu9W6t1W3rWacK6Gw5MY8p9MDvqKAA+YFyG +8rQpSm9obiBEb2UgKFRlc3Qga2V5IDEpIDxqb2huLmRvZUB0ZXN0LmNvbT6JAT4E +EwECACgFAlW6eXgCGwMFCQAnjQAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJ +EJszz8e7cNr6fksH/2Yv+988koBTEYL1/CJo+0Hhq8MEiA1uXdOkZnD1w9Dfhl74 +HlyGDg/DIuaGKgJUd6C2CVFKuEKBlyK+g6ryNAq9z+FitNlQHtOJBwKe9o8QMW1R +tSlket8QCD2+vQIMg+NafHzh7l6GLADrQsqwXIfKITS6CW2Xz1XwXEmZB8xR1ro3 +PdmK9Jx+lckCyAy5vc1FMmxMG5MIhxzbjWCyUIOndI1/ITgldO4xmXIZeJ5yuVqd +7XGm1uGo5XB6W8lTRhubAWDPNRDbKG3Rc+zn/lpxW5ilLI8LNPdestsajSwXK88m +LtZgZ/hu8j03I7GkOVRQpvVhWmRPjCp88LHwe1WdA74EVbp5eAEIAOEG0w8PoIcW +fwyy5aOt4G9UwJwOBBJI04KJ4eytpTH3cFqtJi8eun6pakP/+sTetinUyZklMm3z +v1vGesQY69Wpxepfi3ywZiO09HcOd2X/K8wFP8EbAfMaSMQYxJRvLRuP/QilsLjH +4TQMwyfwLxNg41UZ/kaljsnPft96/bxhr8y3VCxYet8uZ6rCHvWGusBaB8zaGn/2 +Z5dIIK9OSuJooVEclOWd6ZRMQ+/HmsjyUzjPk+5h6G8svY+RQarScrnxCQBQh0Z6 +JfXJyH8RCs4PGeAEwcHg73V9T5EFo2YdnAyZhOgFdrUJaptJoFi6jIEVUa41HeO4 +R5VzktvVAekAEQEAAf4DAwJ4oMhGvc8VkGDiMVJi06Vm9dIhYHWd6/o54qhc/lAm +oFcdjWVbfv92JaPTf/OyEVIpKIxBgiS6a0IddalLrTB7gBm5kHvgA+SjyEYv9Wvo +V8a22I3B9qKGutUBfHP1Ij8mhValk1N20Odkf0RniYFK/lyu8LRjX7HmAq7Ze0AW +zpEL7VZVInMpLFxdnj673qTq6UptgHtpU30vXs85TYJf/xYTnbRpj9h2blSQ0WR8 +XwCsFPuHziO6SfzRIOvFhmgX64yhikG7JI6nbMRHaV69Ll4hMgRb3O60LD//bamm +fBxIqXZRJPRU7f6DcojXAHaBD2FLLaq6Mj/UAICOcpHMrLeuH6DzNsPUeUUEvP0r +EOzEYjyx639bzVVgJrzfas9yW50on6ji+xKT9J+HVihKAUEDwcLH42hUq8W6VKPU +VlSLlaMi5qi3swrk8OfTnqIYDB8p8WaEhcv9qSTI/tOposqkM/1YC7XhNNFMWKEf +VXuqs/eDAN7W3VtwMbhymP3hmIOZYwnXM8mZleJexpx1paTtQwPrs7eJFbvoGU7I +3cVAUg8WbR0l5hSUL0Ma7in8P/UJ2JYrJ5QgWh5xAlcxVrBj8SdjF+UA/JNxQhDu +YI35bZnHibfMSMuFp37Gphqv5P5svuPuknYdsXPAeNdH/vTYsBapH6o86TP4+Xc1 +JTcQXZnGHLWA2FeMiG7ttKPspLP4Jtt2K2OQLZjPJgcI++UDTNCs734jwZzIfz31 +0LLmfnRE+avq3+J8z5d7PllEr+6t5Xn70lFCiqY9gmNdLXnGjgkIs2nq+U2xVMFe +k4TMVqpOw1vY6PRlRhp44qJKnJx3GnXuiG0lSBbOlB7ITFaFynsI3/9zGcnoqwMM +g6G5+l/Jm8ZwGyCvxXhb1l5+RFuTwj75uGKUQB3viQElBBgBAgAPBQJVunl4AhsM +BQkAJ40AAAoJEJszz8e7cNr6oXUIANhmSkmEI5rEUmhIz/6KkIV7E7D07OJjYFDI +BtpS1YvPn4h1ki2H765DXGi0+kWilt0qoRTzEjL3+VnICkyMJlpSzwvtXUXFsPiV +TnE5IG2fZVoz9vV5Scd9IjOaY1bqQdv8OxGVf2pQLtk8r+8qLiMUTmIuKzrf+1Pv +apHLc0ciZ3lhIu2CgkgLKkbjqf5RttAHBXUCxEWMm254YSVTx8jCiLQo/01zEcTF +as+ccy4Az91e5v7NyqQa4iSVoWROpsTBqYXdbXRx3FZfktjL6psEXUQ3JDFLf+he +cWFV/GAlPRyR2NvZH5ZuhJVW0uA7uxqD+slfw8rWSSvzHiyWjmQ= +=bM65 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/keysign/gpg/keys/testkey2.sec b/keysign/gpg/keys/testkey2.sec new file mode 100644 index 0000000..fe2dde0 --- /dev/null +++ b/keysign/gpg/keys/testkey2.sec @@ -0,0 +1,35 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +lQH+BFW6gqgBBACmDQ27uMwkzrTm/LZzg2gO1pwRzPK0hRUeB8nfkui2DCmN5/PX +J7GnK57LmGjh/HPnEJSzQQCPY83q8Jt1oVdMXP5rlG98n3FM018Ql6Sn4s8JrOHS +GHT5iyA9TFI6jkWgYSiMss4EZTyaCM6e4vTVawIau03YVu97pO5RRS5HnQARAQAB +/gMDAgXUgrBgyliFYOBLoG2wBXS1UU3c03jQG7sAh0mDA/rXhy4mxVBqvp2r/tFZ ++MTcuVXp429Ys4PqmtaCislY6lRLMvp7MVP3rTL+nFjchnIp7S9806p8OQwABSPK +BN4qzkkFTswH5ElKDvbYQcP4QdWBRwpL38hWbrUhO3T0jG9CwLXE9chLZQ+AAi7K +DHUvSW7hhK/LogWyyDSTleS3fnn1CeS7/q6wJb/n41HXzQQFzV/dT7r4Mp6Ia/uy +JhC6/KITV9VQ8bGgUTpes2J/vxKXDQMVfmtunG0JzMyvalhFrIc0zAKLD+NufDag +F2QVQsnLNpcVWV4TQKbCr5pqcUJtPhhSVY4T+mFsuc9EDDhLVsc/ClWSsh39gvxC +Rm4YPbaNpb7P5laUrx3DOSq7vQEz88+hLGxbglYoEUxcUe+u/4D40lKePy9SUU7B +XDhe/HLK0XSQ+xe5+TVGQ4j7ChXwxPcC3xnczBB33r4ntCtKb2hueSBEb2UgKFRl +c3Qga2V5IDIpIDxqb2hueS5kb2VAdGVzdC5jb20+iL4EEwECACgFAlW6gqgCGwMF +CQAnjQAGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEPHn9N93FXWOzyAD/j1H +0wHY8pJbMnMSWFj3H1EIv2Evxn4YD5OZNNpLaAsmGQ/mq2zh7zcZGAF0MMyaJsf1 +wFlXFtohQ+RPzm6pij/pRH/BH9fxUSFiY2vaiGZEWGa5MfA5B6e33KWViNTFgUXe +gz22BaeD8+HQkCovoTvxLECDLlGJ/aNWST8TlF4FnQH+BFW6gqgBBAC2zByQAq0x +PPwxZmIP6II9uJIsXdxZpO/dmSUHz3nY2bPHux/HUqPu7Ms7IacEfynRRIUh2lct +mprSKjVxkaaz2G8fPRHJtdAO7ch3frezo6slj1ecbHIzCV45Ngcv0vpuRGZdujOT +OM7badnO0m6+JnLDNMQBK3tVwOnLdfAhdQARAQAB/gMDAgXUgrBgyliFYDOoohgh +0oB/cZzaZemiIQnggKuqcz8yUIvxyfDFnafazSJ9VVVKbtkrdG1X//g6wKs/LMIZ +kJmWQTL8zsHbsOtArA1mqXik88RD/D12O/mvGf/wjIGNhmBrXRY3YZ7g6FrGLi7t +TTJDduGGcYn+hI66PJMptqj9hTphY5fUGARajkrsjok4qs0BSBw6RzQSH3rJ51Gw +2L+w2xHQUm6bOcv7Tb3OdeIlK5jWEn/eoWukI8431up82SDBlzUesQHzBmEEJcAN +cBOFVjjBui+m3ESXrCZtXhOHiFniGb+gTvZjuXspr2/KXKQBjDbXBnBRspmbjVJi +4DDDJjpTxA/U+3N+4LpLOdMQ3oDOcJUzY10hiFY+Ybul2Wbd3UrPApzxiid3YLnL +MAp64yEJCRX3UzrEAVD912WVUJQWkYJafZUjOvy6ms3gDm6TI1Jddwm7xjuv9uy8 +yu3NiAbDw0FY5HX4VjeTiKUEGAECAA8FAlW6gqgCGwwFCQAnjQAACgkQ8ef033cV +dY67DQP/eTHis1/kEOhwD3oviQ9P1itWBKzs3vXBE9bEWs7YYJU/0riNp+UXE5mn +3cx7/C8nsRwcH5HssYVxo/b1Z8mHhsOzMqhkWbjRZ31S7vLvaNcl2+n6hJFTykXI +HPhlVFJIMnppWzgt5iKFXQQ56PfmzJRxwQDnBr3qZPICyPj9LM0= +=xYtv +-----END PGP PRIVATE KEY BLOCK----- diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index dcbe3ec..dcf0769 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -115,8 +115,12 @@ def test_gpg_sign_uid(self): with self.keyfile('testkey1.pub') as fp: ctx.import_(fp) + with self.keyfile('signonly.sec') as fp: + secret_key = fp.read() + userId = ctx.get_key('john.doe@test.com').uids[0] - res = gpg.gpg_sign_uid(ctx, tmpdir, userId) + res = gpg.gpg_sign_uid(ctx, tmpdir, userId, secret_key) + self.assertTrue(res) # verify if we have the uid signed From 642fdc8352ce63354fe7c0fac3fb474b5a5839e1 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 22 Aug 2015 12:49:02 +0300 Subject: [PATCH 082/102] Fixed a bug in GPGQRCode.py widget which was causing gks-qrcode script to fail --- keysign/GPGQRCode.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/keysign/GPGQRCode.py b/keysign/GPGQRCode.py index 756314a..53d50a4 100755 --- a/keysign/GPGQRCode.py +++ b/keysign/GPGQRCode.py @@ -21,18 +21,22 @@ for keys and selects the one matching your input """ from gi.repository import Gtk -from keysign.gpg.gpg import GetNewKeyring +import gpgme +from gpg import gpg from QRCode import QRImage def main(): import sys + if len(sys.argv) < 2: + print ("Usage: script.py ") + sys.exit(0) key = sys.argv[1] - keyring = GetNewKeyring() - keys = keyring.get_keys(key) + keyring = gpgme.Context() + keys = gpg.gpg_get_keylist(keyring, key) # Heh, we take the first key here. Maybe we should raise a warning # or so, when there is more than one key. - fpr = keys.items()[0][0] + fpr = keys[0].subkeys[0].fpr data = 'OPENPGP4FPR:' + fpr w = Gtk.Window() From 01edc64d3abbb1b2688562da54760a482b649683 Mon Sep 17 00:00:00 2001 From: Tobias Mueller Date: Wed, 2 Sep 2015 10:58:27 +0200 Subject: [PATCH 083/102] test: imported StringIO from IO for Python3 compatibility --- keysign/gpg/test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index dcf0769..819fff5 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -27,8 +27,7 @@ import unittest -from io import BytesIO -from StringIO import StringIO +from io import BytesIO, StringIO __all__ = ['GpgTestSuite'] From 99f66052d9316fbaa0793fab156cf05c363df464 Mon Sep 17 00:00:00 2001 From: Tobias Mueller Date: Wed, 2 Sep 2015 14:59:48 +0200 Subject: [PATCH 084/102] gpg: Do not encode keydata on import We need a BytesIO from bytes, anyway. We need to make our callers provide us bytes. One approach could be to pre-emptively attempt to encode keydata and catch a non-existant encode function. But let's try with the "pure" approach first. --- keysign/Sections.py | 6 +++++- keysign/gpg/gpg.py | 6 ++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index b9a0ed2..b9cb7b8 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -296,6 +296,9 @@ def on_barcode(self, sender, barcode, message=None): def download_key_http(self, address, port): + '''Downloads a key from a keyserver and provides + bytes (as opposed to an unencoded string). + ''' url = ParseResult( scheme='http', # This seems to work well enough with both IPv6 and IPv4 @@ -304,8 +307,9 @@ def download_key_http(self, address, port): params='', query='', fragment='') - return requests.get(url.geturl()).text + return requests.get(url.geturl()).text.encode('utf-8') + def try_download_keys(self, clients): for client in clients: self.log.debug("Getting key from client %s", client) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 5e520c7..58f4288 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -102,13 +102,11 @@ def gpg_import_key(gpgmeContext, fpr): def gpg_import_keydata(gpgmeContext, keydata): - """Tries to import a OpenPGP key from @keydata + """Tries to import a OpenPGP key from @keydata. + Keydata needs to be bytes (or an encoded string). The @gpgmeContext object has a gpg directory already set. """ - # XXX: PyGPGME key imports doesn't work with data as unicode strings - # but here we get data coming from network which is unicode - keydata = keydata.encode('utf-8') keydataIO = BytesIO(keydata) result = None try: From 4763a9753b30f0ae5ed325723f8cc15abf8d6705 Mon Sep 17 00:00:00 2001 From: Tobias Mueller Date: Wed, 2 Sep 2015 15:30:25 +0200 Subject: [PATCH 085/102] Sections: Debugging the keylist after downloading the key For now, the keylist seems to be empty. So we print the contents out. --- keysign/Sections.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index b9cb7b8..f05cc5a 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -576,5 +576,7 @@ def on_button_clicked(self, button, *args, **kwargs): def recieved_key(self, fingerprint, keydata, *data): self.received_key_data = keydata - gpgmeKey = gpg.gpg_get_keylist(self.ctx, fingerprint, False)[0] + keylist = gpg.gpg_get_keylist(self.ctx, fingerprint, False) + self.log.debug('Getting keylist: %r', keylist) + gpgmeKey = keylist[0] self.signPage.display_downloaded_key(gpg.gpg_format_key(gpgmeKey)) From 779217dddc71ab10f5b59356db32acc4f99cd561 Mon Sep 17 00:00:00 2001 From: Tobias Mueller Date: Wed, 2 Sep 2015 15:35:50 +0200 Subject: [PATCH 086/102] Sections: Made it work again by not deleting the keyring too early We could not sign anyones key, because the downloaded key was deleted too early. Now, we don't delete the key so that we can sign it. I added a FIXME to think about removing the directory later. --- keysign/Sections.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index f05cc5a..970ec44 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -381,8 +381,11 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) self.log.debug('Adding %s as callback', callback) GLib.idle_add(callback, fingerprint, keydata, data) - # Remove the temporary keyring - gpg.gpg_reset_engine(self.ctx, tmp_gpghome) + # FIXME: Remove the temporary keyring. + # We cannot do it right now, because the key is signed + # later! If we delete the directory now, we cannot sign + # it... + #gpg.gpg_reset_engine(self.ctx, tmp_gpghome) self.log.info("Deleting temporary gpg home dir: %s", tmp_gpghome) # If this function is added itself via idle_add, then idle_add will From dc0952d01480de9430caa00c821b253950e4330c Mon Sep 17 00:00:00 2001 From: Tobias Mueller Date: Wed, 2 Sep 2015 15:46:35 +0200 Subject: [PATCH 087/102] gpg test: Asserted that we have more signatures than before Not hard code the number. It was 0 and 1 for me, so the test failed. I think that if we are asserting that we have more signatures after signing than before, we're good to go. --- keysign/gpg/test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index 819fff5..a16a2a2 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -118,13 +118,15 @@ def test_gpg_sign_uid(self): secret_key = fp.read() userId = ctx.get_key('john.doe@test.com').uids[0] + len_before = len(userId.signatures) res = gpg.gpg_sign_uid(ctx, tmpdir, userId, secret_key) self.assertTrue(res) # verify if we have the uid signed sigs = ctx.get_key('john.doe@test.com').uids[0].signatures - self.assertEqual(len(sigs), 2) #we're counting the self signature + len_after = len(sigs) + self.assertGreater(len_after, len_before) if __name__ == '__main__': From dcf0e3f9a664f59fc3d65fe4b24448276d27a9ec Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 14 Sep 2015 19:22:30 +0300 Subject: [PATCH 088/102] Sections: Now it also removes the gpg tmpdir after saving the downloaded key. --- keysign/Sections.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 970ec44..11d2844 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -255,6 +255,8 @@ def __init__(self, app): # A list holding references to temporary files which should probably # be cleaned up on exit... self.tmpfiles = [] + # A path to a tmp gpg homedir + self.tmp_gpghome = None def set_progress_bar(self): page_index = self.notebook.get_current_page() @@ -309,7 +311,7 @@ def download_key_http(self, address, port): fragment='') return requests.get(url.geturl()).text.encode('utf-8') - + def try_download_keys(self, clients): for client in clients: self.log.debug("Getting key from client %s", client) @@ -352,7 +354,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) # For each key downloaded we create a new gpgme.Context object and # set up a temporary dir for gpg self.ctx = gpgme.Context() - tmp_gpghome = gpg.gpg_set_engine(self.ctx, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix='tmp.gpghome') + self.tmp_gpghome = gpg.gpg_set_engine(self.ctx, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix='tmp.gpghome') other_clients = self.sort_clients(other_clients, fingerprint) @@ -381,13 +383,6 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) self.log.debug('Adding %s as callback', callback) GLib.idle_add(callback, fingerprint, keydata, data) - # FIXME: Remove the temporary keyring. - # We cannot do it right now, because the key is signed - # later! If we delete the directory now, we cannot sign - # it... - #gpg.gpg_reset_engine(self.ctx, tmp_gpghome) - self.log.info("Deleting temporary gpg home dir: %s", tmp_gpghome) - # If this function is added itself via idle_add, then idle_add will # keep adding this function to the loop until this func ret False return False @@ -581,5 +576,10 @@ def recieved_key(self, fingerprint, keydata, *data): self.received_key_data = keydata keylist = gpg.gpg_get_keylist(self.ctx, fingerprint, False) self.log.debug('Getting keylist: %r', keylist) + # Delete temporary dir after we're done getting the key + if self.tmp_gpghome: + gpg.gpg_reset_engine(self.ctx, self.tmp_gpghome) + self.log.info("Deleting tmp gpg homedir: %s", self.tmp_gpghome) + gpgmeKey = keylist[0] self.signPage.display_downloaded_key(gpg.gpg_format_key(gpgmeKey)) From 8df5ed2e3bdee87be6d9b1d5facd162fa0518c12 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Fri, 2 Oct 2015 01:55:20 +0300 Subject: [PATCH 089/102] GPGQRCode: change error message and exit value --- keysign/GPGQRCode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keysign/GPGQRCode.py b/keysign/GPGQRCode.py index 53d50a4..df5030f 100755 --- a/keysign/GPGQRCode.py +++ b/keysign/GPGQRCode.py @@ -29,8 +29,8 @@ def main(): import sys if len(sys.argv) < 2: - print ("Usage: script.py ") - sys.exit(0) + print ("Usage: {} ".format(sys.argv[0])) + sys.exit(1) key = sys.argv[1] keyring = gpgme.Context() keys = gpg.gpg_get_keylist(keyring, key) From 67195f2337a3bc4aa84a752b0e97764b82c08d58 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sat, 9 Apr 2016 01:31:14 +0300 Subject: [PATCH 090/102] Sections: Changed download_key_http to return bytes through content field --- keysign/Sections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 11d2844..f619b31 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -309,7 +309,7 @@ def download_key_http(self, address, port): params='', query='', fragment='') - return requests.get(url.geturl()).text.encode('utf-8') + return requests.get(url.geturl()).content def try_download_keys(self, clients): From 7d2107aad2a105ea4b5602a9551b0e24022038b2 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 18 Apr 2016 21:19:03 +0300 Subject: [PATCH 091/102] gpg: gpg_get_keylist can receive extra argument for key expired If set to True the function will return expired keys also, else it won't. This is how we can implement the check for expiration, because the gpgme module that we use doesn't recognize the expired keys (aka the key.expired is always set to False). Also, the 'expiration date' can only be retrieved from the key.subkeys[0].expires field ... wierdly! --- keysign/gpg/gpg.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 58f4288..49b7977 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -29,6 +29,7 @@ import gpgme import gpgme.editutil from io import BytesIO +from datetime import datetime try: from StringIO import StringIO except ImportError: @@ -116,12 +117,18 @@ def gpg_import_keydata(gpgmeContext, keydata): return result -def gpg_get_keylist(gpgmeContext, keyid=None, secret=False): +def gpg_get_keylist(gpgmeContext, keyid=None, secret=False, expired=False): """Returns the keys found in @gpgmeContext If @keyid is None then all geys will be returned. If @secret=True then it will return the secret keys. """ - keys = [key for key in gpgmeContext.keylist(keyid, secret)] + keys = [] + for key in gpgmeContext.keylist(keyid, secret): + exp_date = datetime.fromtimestamp(float(key.subkeys[0].expires)) + if expired == False and key.subkeys[0].expires != 0 and exp_date.date() < datetime.today().date(): + continue + keys.append(key) + return keys From 32902ae6717eaaef52cd5557b81b7d7df8ce2095 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Mon, 18 Apr 2016 23:30:36 +0300 Subject: [PATCH 092/102] Removed the gpg_ prefix from gpg functions. The gpg module is instended to be imported and used as a namespace for calling the module functions, that's why it is unnecessary to have a gpg_ prefix. --- keysign/GPGQRCode.py | 2 +- keysign/KeyPresent.py | 2 +- keysign/KeysPage.py | 2 +- keysign/Sections.py | 26 +++++++++++++------------- keysign/SignPages.py | 2 +- keysign/gpg/gpg.py | 32 +++++++++++++++++--------------- keysign/gpg/test.py | 20 ++++++++++---------- 7 files changed, 44 insertions(+), 42 deletions(-) diff --git a/keysign/GPGQRCode.py b/keysign/GPGQRCode.py index df5030f..30e5c3c 100755 --- a/keysign/GPGQRCode.py +++ b/keysign/GPGQRCode.py @@ -33,7 +33,7 @@ def main(): sys.exit(1) key = sys.argv[1] keyring = gpgme.Context() - keys = gpg.gpg_get_keylist(keyring, key) + keys = gpg.get_keylist(keyring, key) # Heh, we take the first key here. Maybe we should raise a warning # or so, when there is more than one key. fpr = keys[0].subkeys[0].fpr diff --git a/keysign/KeyPresent.py b/keysign/KeyPresent.py index 0add80a..081ff95 100644 --- a/keysign/KeyPresent.py +++ b/keysign/KeyPresent.py @@ -133,7 +133,7 @@ def main(args=sys.argv): #if arguments.gpg: # keyid = arguments.file # ctx = gpgme.Context() - # found_keys = gpg.gpg_get_keylist(ctx, keyid) + # found_keys = gpg.get_keylist(ctx, keyid) # # We take the first item we found and export the actual keydata # fpr = found_keys[0].subkeys[0].fpr # keydata = gpg.extract_keydata(ctx, fpr, True) diff --git a/keysign/KeysPage.py b/keysign/KeysPage.py index 91584b4..34755c4 100644 --- a/keysign/KeysPage.py +++ b/keysign/KeysPage.py @@ -79,7 +79,7 @@ def __init__(self, show_public_keys=False): self.keysDict = {} # FIXME: implement a callback that refreshes the UIDs when they change - for key in gpg.gpg_get_keylist(self.ctx, None, True): + for key in gpg.get_keylist(self.ctx, None, True): if key.revoked or key.expired or key.invalid or key.subkeys[0].disabled: continue diff --git a/keysign/Sections.py b/keysign/Sections.py index f619b31..3fa4ede 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -326,7 +326,7 @@ def try_download_keys(self, clients): def verify_downloaded_key(self, downloaded_data, fingerprint): # FIXME: implement a better and more secure way to verify the key - res = gpg.gpg_import_keydata(self.ctx, downloaded_data) + res = gpg.import_keydata(self.ctx, downloaded_data) if res and len(res.imports): (imported_key_fpr, null, null) = res.imports[0] if imported_key_fpr == fingerprint: @@ -354,7 +354,7 @@ def obtain_key_async(self, fingerprint, callback=None, data=None, error_cb=None) # For each key downloaded we create a new gpgme.Context object and # set up a temporary dir for gpg self.ctx = gpgme.Context() - self.tmp_gpghome = gpg.gpg_set_engine(self.ctx, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix='tmp.gpghome') + self.tmp_gpghome = gpg.set_engine(self.ctx, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix='tmp.gpghome') other_clients = self.sort_clients(other_clients, fingerprint) @@ -393,7 +393,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.debug("I will sign key with fpr {}".format(fingerprint)) ctx = gpgme.Context() - gpg_homedir = gpg.gpg_set_engine(ctx) + gpg_homedir = gpg.set_engine(ctx) keydata = data or self.received_key_data # FIXME: until this (https://code.launchpad.net/~daniele-athome/pygpgme/pygpgme/+merge/173333) @@ -406,9 +406,9 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): # 1. Fetch the key into a temporary keyring self.log.debug('Trying to import key\n%s', keydata) - if gpg.gpg_import_keydata(ctx, keydata): + if gpg.import_keydata(ctx, keydata): - keys = gpg.gpg_get_keylist(ctx, fingerprint) + keys = gpg.get_keylist(ctx, fingerprint) self.log.info("Found keys %s for fp %s", keys, fingerprint) assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) @@ -420,7 +420,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): uid_str = uid.uid self.log.info("Processing uid %s %s", uid, uid_str) - res = gpg.gpg_sign_uid(ctx, gpg_homedir, uid) + res = gpg.sign_uid(ctx, gpg_homedir, uid) if not res: # we may have already signed this uid before self.log.info("Uid %s was signed before.\nUpdating signature made by key: %s", @@ -430,7 +430,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): signed_key = gpg.export_key(ctx, fingerprint, True) self.log.info("Exported %d bytes of signed key", len(signed_key)) - encrypted_key = gpg.gpg_encrypt_data(ctx, signed_key, uid_str) + encrypted_key = gpg.encrypt_data(ctx, signed_key, uid_str) keyid = key.subkeys[0].fpr[-8:] template_ctx = { @@ -469,8 +469,8 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.debug('You are signing one of your own keys: %s', key.subkeys[0].fpr) ctx.delete(key, True) - gpg.gpg_import_keydata(ctx, keydata) - keys = gpg.gpg_get_keylist(ctx, fingerprint) + gpg.import_keydata(ctx, keydata) + keys = gpg.get_keylist(ctx, fingerprint) self.log.info("Found keys %s for fp %s", keys, fingerprint) assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) @@ -488,7 +488,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): GLib.idle_add(error_cb, data) # We are done signing the key so we remove the temporary keyring - gpg.gpg_reset_engine(ctx, gpg_homedir) + gpg.reset_engine(ctx, gpg_homedir) self.log.info("Deleting temporary gpg home dir: %s", gpg_homedir) return False @@ -574,12 +574,12 @@ def on_button_clicked(self, button, *args, **kwargs): def recieved_key(self, fingerprint, keydata, *data): self.received_key_data = keydata - keylist = gpg.gpg_get_keylist(self.ctx, fingerprint, False) + keylist = gpg.get_keylist(self.ctx, fingerprint, False) self.log.debug('Getting keylist: %r', keylist) # Delete temporary dir after we're done getting the key if self.tmp_gpghome: - gpg.gpg_reset_engine(self.ctx, self.tmp_gpghome) + gpg.reset_engine(self.ctx, self.tmp_gpghome) self.log.info("Deleting tmp gpg homedir: %s", self.tmp_gpghome) gpgmeKey = keylist[0] - self.signPage.display_downloaded_key(gpg.gpg_format_key(gpgmeKey)) + self.signPage.display_downloaded_key(gpg.format_key(gpgmeKey)) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index 7c89597..b0cd8f5 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -58,7 +58,7 @@ def signatures_for_keyid(keyid, context=None): else: ctx = _context if _context else gpgme.Context() - sigs = gpg.gpg_get_siglist(ctx, keyid) + sigs = gpg.get_siglist(ctx, keyid) siglist = [] for sig in sigs: diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 49b7977..4c8169a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -38,7 +38,7 @@ log = logging.getLogger(__name__) -def gpg_set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=None): +def set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=None): """Sets up a temporary directory as new gnupg home for this context """ @@ -48,7 +48,7 @@ def gpg_set_engine(gpgmeContext, protocol=gpgme.PROTOCOL_OpenPGP, dir_prefix=Non return temp_dir -def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP): +def reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP): """Resets the gnupg homedir for the received context """ gpgmeContext.set_engine_info(protocol, None, None) @@ -56,7 +56,7 @@ def gpg_reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP shutil.rmtree(tmp_dir, ignore_errors=True) -def gpg_import_private_key(gpgmeContext, secret_key=None): +def import_private_key(gpgmeContext, secret_key=None): """Imports the user's private key from @secret_key or the default keyring to a temporary context. """ @@ -77,12 +77,12 @@ def gpg_import_private_key(gpgmeContext, secret_key=None): for key in keys: if not key.revoked and not key.expired and not key.invalid and not key.subkeys[0].disabled: - gpg_import_key(gpgmeContext, key.subkeys[0].fpr) + import_key(gpgmeContext, key.subkeys[0].fpr) log.debug("imported your personal key: %s to tmp keyring", key.subkeys[0].fpr) -def gpg_import_key(gpgmeContext, fpr): +def import_key(gpgmeContext, fpr): """Imports a key from the user's keyring into the keyring received as argument. @@ -102,7 +102,7 @@ def gpg_import_key(gpgmeContext, fpr): return len(res.imports) != 0 -def gpg_import_keydata(gpgmeContext, keydata): +def import_keydata(gpgmeContext, keydata): """Tries to import a OpenPGP key from @keydata. Keydata needs to be bytes (or an encoded string). @@ -117,10 +117,12 @@ def gpg_import_keydata(gpgmeContext, keydata): return result -def gpg_get_keylist(gpgmeContext, keyid=None, secret=False, expired=False): +def get_keylist(gpgmeContext, keyid=None, secret=False, expired=False): """Returns the keys found in @gpgmeContext - If @keyid is None then all geys will be returned. - If @secret=True then it will return the secret keys. + + @keyid: the unique id of a key. + @secret: if set to True it returns the secret keys, else it returns the public keys. + @expired: if set to True it also returns the expired keys. """ keys = [] for key in gpgmeContext.keylist(keyid, secret): @@ -132,7 +134,7 @@ def gpg_get_keylist(gpgmeContext, keyid=None, secret=False, expired=False): return keys -def gpg_get_siglist(gpgmeContext, keyid): +def get_siglist(gpgmeContext, keyid): '''Returns a list with all signatures for this @keyid ''' siglist = set() @@ -146,7 +148,7 @@ def gpg_get_siglist(gpgmeContext, keyid): return list(siglist) -def gpg_sign_uid(gpgmeContext, gpg_homedir, userId, secret_key=None): +def sign_uid(gpgmeContext, gpg_homedir, userId, secret_key=None): """Signs a specific uid of a OpenPGP key gpgmeContext: the temporary keyring @@ -158,7 +160,7 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId, secret_key=None): @return: True/False depending if this uid was signed for the first time by this key """ - gpg_import_private_key(gpgmeContext, secret_key) + import_private_key(gpgmeContext, secret_key) primary_key = [key for key in gpgmeContext.keylist(None, True)][0] gpgmeContext.signers = [primary_key] @@ -191,13 +193,13 @@ def gpg_sign_uid(gpgmeContext, gpg_homedir, userId, secret_key=None): return first_sig -def gpg_encrypt_data(gpgmeContext, data, uid, armor=True): +def encrypt_data(gpgmeContext, data, uid, armor=True): """Encrypts @data for the recipients @uid """ plaintext = BytesIO(data) ciphertext = BytesIO() gpgmeContext.armor = armor - recipients = gpg_get_keylist(gpgmeContext, uid) + recipients = get_keylist(gpgmeContext, uid) gpgmeContext.encrypt(recipients, gpgme.ENCRYPT_ALWAYS_TRUST, plaintext, ciphertext) @@ -235,7 +237,7 @@ def format_fpr(fpr): return s -def gpg_format_key(gpgmeKey): +def format_key(gpgmeKey): """Returns a string representation of @gpgmeKey It contains info about: length of key, keyid, expiration date, creation date, diff --git a/keysign/gpg/test.py b/keysign/gpg/test.py index a16a2a2..f09ec01 100644 --- a/keysign/gpg/test.py +++ b/keysign/gpg/test.py @@ -40,11 +40,11 @@ class GpgTestSuite(unittest.TestCase): def keyfile(self, key): return open(os.path.join(keydir, key), 'rb') - def test_gpg_set_engine(self): + def test_set_engine(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') - gpg.gpg_set_engine(ctx, gpgme.PROTOCOL_OpenPGP, tmpdir) + gpg.set_engine(ctx, gpgme.PROTOCOL_OpenPGP, tmpdir) keys = [key for key in ctx.keylist()] @@ -54,7 +54,7 @@ def test_gpg_set_engine(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_reset_engine(self): + def test_reset_engine(self): ctx = gpgme.Context() # set a temporary dir for gpg home tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') @@ -62,7 +62,7 @@ def test_gpg_reset_engine(self): # check if we have created the new gpg dir self.assertTrue(os.path.isdir(tmpdir)) - gpg.gpg_reset_engine(ctx, tmpdir, gpgme.PROTOCOL_OpenPGP) + gpg.reset_engine(ctx, tmpdir, gpgme.PROTOCOL_OpenPGP) self.assertEqual(gpgme.PROTOCOL_OpenPGP, ctx.protocol) self.assertFalse(os.path.exists(tmpdir)) @@ -70,12 +70,12 @@ def test_gpg_reset_engine(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_import_private_key(self): + def test_import_private_key(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) - gpg.gpg_import_private_key(ctx) + gpg.import_private_key(ctx) # get the user's secret keys default_ctx = gpgme.Context() @@ -92,7 +92,7 @@ def test_gpg_import_private_key(self): # clean temporary dir shutil.rmtree(tmpdir, ignore_errors=True) - def test_gpg_import_keydata(self): + def test_import_keydata(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) @@ -100,13 +100,13 @@ def test_gpg_import_keydata(self): with self.keyfile('testkey1.pub') as fp: keydata = fp.read() - res = gpg.gpg_import_keydata(ctx, keydata) + res = gpg.import_keydata(ctx, keydata) self.assertTrue(res) # can we get the key ? key = ctx.get_key('john.doe@test.com') - def test_gpg_sign_uid(self): + def test_sign_uid(self): ctx = gpgme.Context() tmpdir = tempfile.mkdtemp(prefix='tmp.gpghome') ctx.set_engine_info(gpgme.PROTOCOL_OpenPGP, None, tmpdir) @@ -119,7 +119,7 @@ def test_gpg_sign_uid(self): userId = ctx.get_key('john.doe@test.com').uids[0] len_before = len(userId.signatures) - res = gpg.gpg_sign_uid(ctx, tmpdir, userId, secret_key) + res = gpg.sign_uid(ctx, tmpdir, userId, secret_key) self.assertTrue(res) From faa997c0f46080329766e7a79c8210df1c54020d Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 31 May 2016 01:15:56 +0300 Subject: [PATCH 093/102] Sections: updated the way we get the imported key. --- keysign/Sections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 3fa4ede..3dc4c01 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -328,7 +328,7 @@ def verify_downloaded_key(self, downloaded_data, fingerprint): # FIXME: implement a better and more secure way to verify the key res = gpg.import_keydata(self.ctx, downloaded_data) if res and len(res.imports): - (imported_key_fpr, null, null) = res.imports[0] + imported_key_fpr = res.imports[0][0] if imported_key_fpr == fingerprint: result = True else: From 486dfdef3573750def3cce53b46f3ddbad76dbe3 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 31 May 2016 01:52:05 +0300 Subject: [PATCH 094/102] Sections: Updated the in code comments for signing keys --- keysign/Sections.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 3dc4c01..90a5aad 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -462,7 +462,15 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): body=body, files=[filename]) - # we have to re-import the key to have each UID signed individually + # FIXME: Find a better and cleaner way to do this ... + # This is a sort of ugly hack for us to sign each UID individually and send an email + # with only that UID signed. The reason is that we cannot delete a signature from a key + # that is imported in the temp gpg context. So , to bypass this, we do: + # 1. Sign a UID for the key + # 2. Send an email with the key having only that UID signed + # 3. Delete and re-import the key from the local context + # 4. Re-fetch the key from the local keylist + # 5. Repeat try: ctx.delete(key) except gpgme.GpgmeError as exp: @@ -474,6 +482,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.info("Found keys %s for fp %s", keys, fingerprint) assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) + # Because we have re-import the key after each UID sign, we must update the reference key = keys[0] # FIXME: Can we get rid of self.tmpfiles here already? Even if the MUA is still running? From 620ec60c099f7be91a1089801df508f7ad19a19e Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 31 May 2016 18:05:18 +0300 Subject: [PATCH 095/102] Sections: moved log call before the respective function --- keysign/Sections.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keysign/Sections.py b/keysign/Sections.py index 90a5aad..5525272 100644 --- a/keysign/Sections.py +++ b/keysign/Sections.py @@ -482,7 +482,7 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): self.log.info("Found keys %s for fp %s", keys, fingerprint) assert len(keys) == 1, "We received multiple keys for fp %s: %s" % (fingerprint, keys) - # Because we have re-import the key after each UID sign, we must update the reference + # Because we have to re-import the key after each UID sign, we must update the reference key = keys[0] # FIXME: Can we get rid of self.tmpfiles here already? Even if the MUA is still running? @@ -497,8 +497,9 @@ def sign_key_async(self, fingerprint, callback=None, data=None, error_cb=None): GLib.idle_add(error_cb, data) # We are done signing the key so we remove the temporary keyring - gpg.reset_engine(ctx, gpg_homedir) self.log.info("Deleting temporary gpg home dir: %s", gpg_homedir) + gpg.reset_engine(ctx, gpg_homedir) + return False From 30ed79b8ea721628844bfa2a42d86569a2df099a Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 31 May 2016 18:08:31 +0300 Subject: [PATCH 096/102] SignPages: fix the init for the cached context --- keysign/SignPages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index b0cd8f5..caf96a7 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -56,7 +56,8 @@ def signatures_for_keyid(keyid, context=None): if context is not None: ctx = context else: - ctx = _context if _context else gpgme.Context() + _context = _context if _context else gpgme.Context() + ctx = _context sigs = gpg.get_siglist(ctx, keyid) From 5199d5728e8585d5806aca23aa5acee8648f736c Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 31 May 2016 18:24:15 +0300 Subject: [PATCH 097/102] SignPages: use list comprehension for creating sigs list --- keysign/SignPages.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/keysign/SignPages.py b/keysign/SignPages.py index caf96a7..71ed454 100644 --- a/keysign/SignPages.py +++ b/keysign/SignPages.py @@ -60,10 +60,7 @@ def signatures_for_keyid(keyid, context=None): ctx = _context sigs = gpg.get_siglist(ctx, keyid) - - siglist = [] - for sig in sigs: - siglist.append((sig.keyid, sig.timestamp, sig.uid)) + siglist = [(sig.keyid, sig.timestamp, sig.uid) for sig in sigs] return siglist From 26744912c4fbe7605518cbaa5a84151763d05685 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Thu, 2 Jun 2016 18:14:24 +0300 Subject: [PATCH 098/102] gpg: update argument name to be more specific --- keysign/gpg/gpg.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 4c8169a..0063b5a 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -117,17 +117,17 @@ def import_keydata(gpgmeContext, keydata): return result -def get_keylist(gpgmeContext, keyid=None, secret=False, expired=False): +def get_keylist(gpgmeContext, keyid=None, secret=False, expire_flag=False): """Returns the keys found in @gpgmeContext @keyid: the unique id of a key. @secret: if set to True it returns the secret keys, else it returns the public keys. - @expired: if set to True it also returns the expired keys. + @expire_flag: if set to True it also return the expired keys. """ keys = [] for key in gpgmeContext.keylist(keyid, secret): exp_date = datetime.fromtimestamp(float(key.subkeys[0].expires)) - if expired == False and key.subkeys[0].expires != 0 and exp_date.date() < datetime.today().date(): + if expire_flag == False and key.subkeys[0].expires != 0 and exp_date.date() < datetime.today().date(): continue keys.append(key) From 8d9e4341ffee5ce826f53a57ad5e645d092115d6 Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 5 Jun 2016 15:32:49 +0300 Subject: [PATCH 099/102] gpg: Add lib function that returns user's personal keys. The function `get_personal_keys` will return the personal keys from User's defaul keyring. It can return the secret part or the public part of the key. More logic (for key validaiton) can be done at the caller. This commit also fixes issue #16. --- keysign/KeysPage.py | 4 ++-- keysign/gpg/gpg.py | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/keysign/KeysPage.py b/keysign/KeysPage.py index 34755c4..3a2beb0 100644 --- a/keysign/KeysPage.py +++ b/keysign/KeysPage.py @@ -79,8 +79,8 @@ def __init__(self, show_public_keys=False): self.keysDict = {} # FIXME: implement a callback that refreshes the UIDs when they change - for key in gpg.get_keylist(self.ctx, None, True): - if key.revoked or key.expired or key.invalid or key.subkeys[0].disabled: + for key in gpg.get_personal_keys(): + if key.revoked or key.expired or key.invalid or key.disabled or key.subkeys[0].disabled: continue uidslist = key.uids diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 0063b5a..58d3c86 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -122,7 +122,7 @@ def get_keylist(gpgmeContext, keyid=None, secret=False, expire_flag=False): @keyid: the unique id of a key. @secret: if set to True it returns the secret keys, else it returns the public keys. - @expire_flag: if set to True it also return the expired keys. + @expire_flag: if set to True it also returns the expired keys. """ keys = [] for key in gpgmeContext.keylist(keyid, secret): @@ -133,6 +133,29 @@ def get_keylist(gpgmeContext, keyid=None, secret=False, expire_flag=False): return keys +def get_personal_keys(keyid=None, secret=False): + """Returns the personal keys found in User's own keyring. + + @keyid: the unique id of a key. + @secret: if set to True it returns the secret part of the keys, + else it returns the public part. + """ + ctx = gpgme.Context() # the user's personal keyring + secret_personal_keys = [key for key in ctx.keylist(keyid, True)] + if secret: + return secret_personal_keys + + all_public_keys = [key for key in ctx.keylist(None, False)] + + public_personal_keys = [] + for key in all_public_keys: + for skey in secret_personal_keys: + if key.subkeys[0].fpr == skey.subkeys[0].fpr: + public_personal_keys.append(key) + break + + return public_personal_keys + def get_siglist(gpgmeContext, keyid): '''Returns a list with all signatures for this @keyid From bff37ea8ac52dfc71e8b92a08f6ee79fc1567dbc Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Sun, 5 Jun 2016 20:39:44 +0300 Subject: [PATCH 100/102] gpg: Update lib function and remove validation from within it. The get_keylist lib function will now receive a `pair` argument. This is used to differentiate between public keys that are imported from other users/keyrings, and public keys that have a secret part existent inside that keyring. --- keysign/gpg/gpg.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 58d3c86..9ced0a5 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -117,21 +117,28 @@ def import_keydata(gpgmeContext, keydata): return result -def get_keylist(gpgmeContext, keyid=None, secret=False, expire_flag=False): +def get_keylist(gpgmeContext, keyid=None, secret=False, pair=False): """Returns the keys found in @gpgmeContext @keyid: the unique id of a key. @secret: if set to True it returns the secret keys, else it returns the public keys. - @expire_flag: if set to True it also returns the expired keys. + @pair: if True it returns only the public keys that are associated with a secret key """ - keys = [] - for key in gpgmeContext.keylist(keyid, secret): - exp_date = datetime.fromtimestamp(float(key.subkeys[0].expires)) - if expire_flag == False and key.subkeys[0].expires != 0 and exp_date.date() < datetime.today().date(): - continue - keys.append(key) - - return keys + secret_keys = [key for key in gpgmeContext.keylist(keyid, True)] + if secret: + return secret_keys + + public_keys = [key for key in gpgmeContext.keylist(keyid, False)] + if pair: + pair_keys = [] + for key in public_keys: + for skey in secret_keys: + if key.subkeys[0].fpr == skey.subkeys[0].fpr: + pair_keys.append(key) + break + return pair_keys + + return public_keys def get_personal_keys(keyid=None, secret=False): """Returns the personal keys found in User's own keyring. From 4509c43a9bf5c2ef42da2b42ddd53f2b6d7b080e Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jun 2016 19:00:19 +0300 Subject: [PATCH 101/102] gpg: Update message about export private key thread --- keysign/gpg/gpg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index 9ced0a5..ad5e4f7 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -63,9 +63,10 @@ def import_private_key(gpgmeContext, secret_key=None): if secret_key: keydata = secret_key else: - # XXX: There is no option to export a private key in GPGME. Latest post about it can - # be found here: https://lists.gnupg.org/pipermail/gnupg-devel/2015-August/030229.html - # We use this hack for now to import user's private key into a temp keyring + # Until the author of pygpgme (James Henstridge) doesn't merge this: + # https://code.launchpad.net/~daniele-athome/pygpgme/pygpgme/+merge/173333 + # , we cannot use the new GPGME features such as private key export or minimal export. + # We use this hack to export the private keys to a file from which we'll import in our keyring. keydata = subprocess.check_output(["gpg", "--armor", "--export-secret-keys"]) with BytesIO(keydata) as fp: From efe80f18c1eb00895583744cf0f592755a53a9ed Mon Sep 17 00:00:00 2001 From: Andrei Macavei Date: Tue, 7 Jun 2016 19:25:27 +0300 Subject: [PATCH 102/102] gpg: Add `export_secret_keys` function. This separates the logic between importing and exporting private keys. The `export_secret_keys` function will return a string with all the private keys found in the current gpg homedir. This is usefull for when we want to sign a key, and we need to import our secret key into a temporary keyring. --- keysign/gpg/gpg.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/keysign/gpg/gpg.py b/keysign/gpg/gpg.py index ad5e4f7..b722ef7 100644 --- a/keysign/gpg/gpg.py +++ b/keysign/gpg/gpg.py @@ -56,6 +56,29 @@ def reset_engine(gpgmeContext, tmp_dir=None, protocol=gpgme.PROTOCOL_OpenPGP): shutil.rmtree(tmp_dir, ignore_errors=True) +def export_secret_keys(): + """Exports the user's private keys. + + This will export the private keys found in the current GnuPG homedir. + """ + + try: + gnupg_home = os.environ['GNUPGHOME'] + except KeyError as err: + gnupg_home = os.environ['HOME'] + '/.gnupg' + + # At least a private key must be inside the current GnuPG homedir + assert len([key for key in gpgme.Context().keylist(None,True)]) != 0, \ + ("Error: no private key found inside gpg homedir: %s" % (gnupg_home,)) + + keydata = subprocess.check_output(["gpg", "--armor", "--export-secret-keys"]) + assert len(keydata) != 0, ("Error: 0-bytes privatekey data exported") + + log.debug("Exported private key data:\n%s",keydata) + + return keydata + + def import_private_key(gpgmeContext, secret_key=None): """Imports the user's private key from @secret_key or the default keyring to a temporary context. @@ -67,7 +90,7 @@ def import_private_key(gpgmeContext, secret_key=None): # https://code.launchpad.net/~daniele-athome/pygpgme/pygpgme/+merge/173333 # , we cannot use the new GPGME features such as private key export or minimal export. # We use this hack to export the private keys to a file from which we'll import in our keyring. - keydata = subprocess.check_output(["gpg", "--armor", "--export-secret-keys"]) + keydata = export_secret_keys() with BytesIO(keydata) as fp: gpgmeContext.import_(fp)