diff options
author | Patrick Totzke <patricktotzke@gmail.com> | 2017-08-15 10:14:54 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-08-15 10:14:54 +0100 |
commit | 33fb2b66ec1fcb22114fd7a424f42eb081f0eae6 (patch) | |
tree | 312ebc7951a609db5fa4fcb25d9558c3922e9b26 | |
parent | 4a8c9c0ba4d763e544ed20d4aa9ce1628da5d8fe (diff) | |
parent | 2d50609739b58de2789f16f1b4e848339e75bce2 (diff) |
Merge pull request #1086 from dcbaker/wip/python-gpg
Use python-gpg instead of pygpgme
-rw-r--r-- | .travis.yml | 45 | ||||
-rw-r--r-- | NEWS | 1 | ||||
-rw-r--r-- | alot/commands/envelope.py | 2 | ||||
-rw-r--r-- | alot/commands/utils.py | 7 | ||||
-rw-r--r-- | alot/crypto.py | 264 | ||||
-rw-r--r-- | alot/db/envelope.py | 9 | ||||
-rw-r--r-- | alot/db/utils.py | 2 | ||||
-rw-r--r-- | alot/errors.py | 2 | ||||
-rw-r--r-- | alot/utils/configobj.py | 2 | ||||
-rw-r--r-- | docs/source/conf.py | 2 | ||||
-rw-r--r-- | docs/source/installation.rst | 6 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | tests/crypto_test.py | 105 | ||||
-rw-r--r-- | tests/utilities.py | 6 |
14 files changed, 253 insertions, 202 deletions
diff --git a/.travis.yml b/.travis.yml index 502671b1..1436aec8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,9 +25,6 @@ env: addons: apt: packages: - # The gpgme build files are needed by the gpgme python module - # (a dependency of alot). - - libgpgme11-dev # The notmuch libs are needed to actually run alot. But currently the # available version is not compatible with alot, so we have to build # from source. @@ -38,6 +35,9 @@ addons: - zlib1g-dev - libgmime-2.6-dev + # dependencies to build the python-gpg module from source + - swig + # Build notmuch and the python notmuch libs manually. The versions of the # notmuch library and the python module which are available in the 12.04 and # 14.04 Ubuntu repos do not match and do not fullfill the version requirement @@ -48,16 +48,19 @@ before_install: | # Build with ccache to speed up rebuilds. export PATH=/usr/lib/ccache:$PATH + # Set paths + export LD_LIBRARY_PATH=$HOME/.local/lib + export PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig + # Clone the notmuch repository and move into it. - git clone git://notmuchmail.org/git/notmuch + git clone git://notmuchmail.org/git/notmuch --depth 1 cd notmuch # Make and install the library. We install the library without sudo as we # might want to switch to the travis container later. ./configure --prefix=$HOME/.local - make + make -j3 -l2 make install # Export the library search path. - export LD_LIBRARY_PATH=$HOME/.local/lib # Install the python bindings. cd bindings/python pip install . @@ -66,6 +69,34 @@ before_install: | pip install coverage codacy-coverage # Move out of the notmuch dir again. cd ../../.. + + # Build GPGME since the version shipping is far too old + + # needs a newer version of gpg-errors + curl https://gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.27.tar.bz2 -o gpgerror.tar.bz2 + tar xvf gpgerror.tar.bz2 + pushd libgpg-error-1.27 + ./configure --prefix=$HOME/.local + make -j3 -l2 + make install + popd + + # and a newer version of libassaun + curl https://gnupg.org/ftp/gcrypt/libassuan/libassuan-2.4.3.tar.bz2 -o assuan.tar.bz2 + tar xf assuan.tar.bz2 + pushd libassuan-2.4.3 + ./configure --prefix=$HOME/.local + make -j3 -l2 + make install + popd + + curl https://gnupg.org/ftp/gcrypt/gpgme/gpgme-1.9.0.tar.bz2 -o gpgme.tar.bz2 + tar xf gpgme.tar.bz2 + pushd gpgme-1.9.0 + ./configure --prefix=$HOME/.local + make -j3 -l2 + make install + popd fi # Prepare a minimal config file for the test. @@ -92,7 +123,7 @@ install: # alot when rebuilding the documentation. Notmuch would have to be # installed by hand in order to get a recent enough version on travis. printf '%s = None\n' NotmuchError NullPointerError > notmuch.py - touch gpgme.py + touch gpg.py # install sphinx for building the html docs pip install sphinx else @@ -5,6 +5,7 @@ next: * bug fix: GPG signatures are acutally verified * feature: option to use linewise focussing in thread mode * feature: add support to move to next or previous message matching a notmuch query in a thread buffer +* feature: Convert from deprecated pygppme module to upstream gpg wrappers 0.5: * save command prompt, recipient and sender history across program restarts diff --git a/alot/commands/envelope.py b/alot/commands/envelope.py index 0294dc01..36ebf9be 100644 --- a/alot/commands/envelope.py +++ b/alot/commands/envelope.py @@ -572,7 +572,7 @@ class EncryptCommand(Command): try: for keyid in self.encrypt_keys: tmp_key = crypto.get_key(keyid) - del envelope.encrypt_keys[crypto.hash_key(tmp_key)] + del envelope.encrypt_keys[tmp_key.fpr] except GPGProblem as e: ui.notify(e.message, priority='error') if not envelope.encrypt_keys: diff --git a/alot/commands/utils.py b/alot/commands/utils.py index f8e5067d..0aa80656 100644 --- a/alot/commands/utils.py +++ b/alot/commands/utils.py @@ -65,9 +65,8 @@ def _get_keys(ui, encrypt_keyids, block_error=False, signed_only=False): :param signed_only: only return keys whose uid is signed (trusted to belong to the key) :type signed_only: bool - :returns: the available keys indexed by their key hash - :rtype: dict(str->gpgme.Key) - + :returns: the available keys indexed by their OpenPGP fingerprint + :rtype: dict(str->gpg key object) """ keys = {} for keyid in encrypt_keyids: @@ -89,5 +88,5 @@ def _get_keys(ui, encrypt_keyids, block_error=False, signed_only=False): else: ui.notify(e.message, priority='error', block=block_error) continue - keys[crypto.hash_key(key)] = key + keys[key.fpr] = key returnValue(keys) diff --git a/alot/crypto.py b/alot/crypto.py index 4da32eea..c73d792c 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -1,43 +1,13 @@ +# encoding=utf-8 # Copyright (C) 2011-2012 Patrick Totzke <patricktotzke@gmail.com> +# Copyright © 2017 Dylan Baker <dylan@pnwbakers.com> # This file is released under the GNU GPL, version 3 or a later revision. # For further details see the COPYING file from __future__ import absolute_import -import os -from cStringIO import StringIO -import gpgme -from .errors import GPGProblem, GPGCode - - -def _hash_algo_name(hash_algo): - """ - Re-implements GPGME's hash_algo_name as long as pygpgme doesn't wrap that - function. +import gpg - :param hash_algo: GPGME hash_algo - :rtype: str - """ - mapping = { - gpgme.MD_MD5: "MD5", - gpgme.MD_SHA1: "SHA1", - gpgme.MD_RMD160: "RIPEMD160", - gpgme.MD_MD2: "MD2", - gpgme.MD_TIGER: "TIGER192", - gpgme.MD_HAVAL: "HAVAL", - gpgme.MD_SHA256: "SHA256", - gpgme.MD_SHA384: "SHA384", - gpgme.MD_SHA512: "SHA512", - gpgme.MD_MD4: "MD4", - gpgme.MD_CRC32: "CRC32", - gpgme.MD_CRC32_RFC1510: "CRC32RFC1510", - gpgme.MD_CRC24_RFC2440: "CRC24RFC2440", - } - if hash_algo in mapping: - return mapping[hash_algo] - else: - raise GPGProblem(("Invalid hash_algo passed to hash_algo_name." - " Please report this as a bug in alot."), - code=GPGCode.INVALID_HASH) +from .errors import GPGProblem, GPGCode def RFC3156_micalg_from_algo(hash_algo): @@ -47,12 +17,16 @@ def RFC3156_micalg_from_algo(hash_algo): GPGME returns hash algorithm names such as "SHA256", but RFC3156 says that programs need to use names such as "pgp-sha256" instead. - :param hash_algo: GPGME hash_algo + :param str hash_algo: GPGME hash_algo + :returns: the lowercase name of of the algorithm with "pgp-" prepended :rtype: str """ # hash_algo will be something like SHA256, but we need pgp-sha256. - hash_algo = _hash_algo_name(hash_algo) - return 'pgp-' + hash_algo.lower() + algo = gpg.core.hash_algo_name(hash_algo) + if algo is None: + raise GPGProblem('Unknown hash algorithm {}'.format(algo), + code=GPGCode.INVALID_HASH_ALGORITHM) + return 'pgp-' + algo.lower() def get_key(keyid, validate=False, encrypt=False, sign=False, @@ -81,69 +55,94 @@ def get_key(keyid, validate=False, encrypt=False, sign=False, :param signed_only: only return keys whose uid is signed (trusted to belong to the key) :type signed_only: bool - :rtype: gpgme.Key + :returns: A gpg key matching the given parameters + :rtype: gpg.gpgme._gpgme_key + :raises ~alot.errors.GPGProblem: if the keyid is ambiguous + :raises ~alot.errors.GPGProblem: if there is no key that matches the + parameters + :raises ~alot.errors.GPGProblem: if a key is found, but signed_only is true + and the key is unused """ - ctx = gpgme.Context() + ctx = gpg.core.Context() try: key = ctx.get_key(keyid) if validate: validate_key(key, encrypt=encrypt, sign=sign) - except gpgme.GpgmeError as e: - if e.code == gpgme.ERR_AMBIGUOUS_NAME: + except gpg.errors.KeyNotFound: + raise GPGProblem('Cannot find key for "{}".'.format(keyid), + code=GPGCode.NOT_FOUND) + except gpg.errors.GPGMEError as e: + if e.getcode() == gpg.errors.AMBIGUOUS_NAME: # When we get here it means there were multiple keys returned by # gpg for given keyid. Unfortunately gpgme returns invalid and # expired keys together with valid keys. If only one key is valid # for given operation maybe we can still return it instead of # raising exception - keys = list_keys(hint=keyid) + valid_key = None - for k in keys: - try: - validate_key(k, encrypt=encrypt, sign=sign) - except GPGProblem: - # if the key is invalid for given action skip it - continue - - if valid_key: - # we have already found one valid key and now we find - # another? We really received an ambiguous keyid + + # Catching exceptions for list_keys + try: + for k in list_keys(hint=keyid): + try: + validate_key(k, encrypt=encrypt, sign=sign) + except GPGProblem: + # if the key is invalid for given action skip it + continue + + if valid_key: + # we have already found one valid key and now we find + # another? We really received an ambiguous keyid + raise GPGProblem( + "More than one key found matching this filter. " + "Please be more specific " + "(use a key ID like 4AC8EE1D).", + code=GPGCode.AMBIGUOUS_NAME) + valid_key = k + except gpg.errors.GPGMEError as e: + # This if will be triggered if there is no key matching at all. + if e.getcode() == gpg.errors.AMBIGUOUS_NAME: raise GPGProblem( - "More than one key found matching this filter. Please " - "be more specific (use a key ID like 4AC8EE1D).", - code=GPGCode.AMBIGUOUS_NAME) - valid_key = k + 'Can not find any key for "{}".'.format(keyid), + code=GPGCode.NOT_FOUND) + raise if not valid_key: # there were multiple keys found but none of them are valid for # given action (we don't have private key, they are expired - # etc) + # etc), or there was no key at all raise GPGProblem( 'Can not find usable key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) return valid_key - elif e.code == gpgme.ERR_INV_VALUE or e.code == gpgme.ERR_EOF: - raise GPGProblem('Can not find key for "{}".'.format(keyid), - code=GPGCode.NOT_FOUND) + elif e.getcode() == gpg.errors.INV_VALUE: + raise GPGProblem( + 'Can not find usable key for "{}".'.format(keyid), + code=GPGCode.NOT_FOUND) else: raise e if signed_only and not check_uid_validity(key, keyid): - raise GPGProblem('Can not find a trusworthy key for "{}".'.format(keyid), + raise GPGProblem('Cannot find a trusworthy key for "{}".'.format(keyid), code=GPGCode.NOT_FOUND) return key def list_keys(hint=None, private=False): """ - Returns a iterator of all keys containing the fingerprint, or all keys if + Returns a generator of all keys containing the fingerprint, or all keys if hint is None. + The generator may raise exceptions of :class:gpg.errors.GPGMEError, and it + is the caller's responsibility to handle them. + :param hint: Part of a fingerprint to usee to search :type hint: str or None :param private: Whether to return public keys or secret keys :type private: bool - :rtype: :class:`gpgme.KeyIter` + :returns: A generator that yields keys. + :rtype: Generator[gpg.gpgme.gpgme_key_t, None, None] """ - ctx = gpgme.Context() + ctx = gpg.core.Context() return ctx.keylist(hint, private) @@ -154,98 +153,69 @@ def detached_signature_for(plaintext_str, key=None): A detached signature in GPG speak is a separate blob of data containing a signature for the specified plaintext. - :param plaintext_str: text to sign - :param key: gpgme_key_t object representing the key to use - :rtype: tuple of gpgme.NewSignature array and str + :param str plaintext_str: text to sign + :param key: key to sign with + :type key: gpg.gpgme._gpgme_key + :returns: A list of signature and the signed blob of data + :rtype: tuple[list[gpg.results.NewSignature], str] """ - ctx = gpgme.Context() - ctx.armor = True + ctx = gpg.core.Context(armor=True) if key is not None: ctx.signers = [key] - plaintext_data = StringIO(plaintext_str) - signature_data = StringIO() - sigs = ctx.sign(plaintext_data, signature_data, gpgme.SIG_MODE_DETACH) - signature_data.seek(0, os.SEEK_SET) - signature = signature_data.read() - return sigs, signature + (sigblob, sign_result) = ctx.sign(plaintext_str, mode=gpg.constants.SIG_MODE_DETACH) + return sign_result.signatures, sigblob def encrypt(plaintext_str, keys=None): - """ - Encrypts the given plaintext string and returns a PGP/MIME compatible - string + """Encrypt data and return the encrypted form. - :param plaintext_str: the mail to encrypt - :param key: gpgme_key_t object representing the key to use - :rtype: a string holding the encrypted mail + :param str plaintext_str: the mail to encrypt + :param key: optionally, a list of keys to encrypt with + :type key: list[gpg.gpgme.gpgme_key_t] or None + :returns: encrypted mail + :rtype: str """ - plaintext_data = StringIO(plaintext_str) - encrypted_data = StringIO() - ctx = gpgme.Context() - ctx.armor = True - ctx.encrypt(keys, gpgme.ENCRYPT_ALWAYS_TRUST, plaintext_data, - encrypted_data) - encrypted_data.seek(0, os.SEEK_SET) - encrypted = encrypted_data.read() - return encrypted + ctx = gpg.core.Context(armor=True) + out = ctx.encrypt(plaintext_str, recipients=keys, always_trust=True)[0] + return out def verify_detached(message, signature): - '''Verifies whether the message is authentic by checking the - signature. + """Verifies whether the message is authentic by checking the signature. - :param message: the message as `str` - :param signature: a `str` containing an OpenPGP signature - :returns: a list of :class:`gpgme.Signature` + :param str message: The message to be verified, in canonical form. + :param str signature: the OpenPGP signature to verify + :returns: a list of signatures + :rtype: list[gpg.results.Signature] :raises: :class:`~alot.errors.GPGProblem` if the verification fails - ''' - message_data = StringIO(message) - signature_data = StringIO(signature) - ctx = gpgme.Context() - - status = ctx.verify(signature_data, message_data, None) - if isinstance(status[0].status, gpgme.GpgmeError): - raise GPGProblem(status[0].status.message, code=status[0].status.code) - return status + """ + ctx = gpg.core.Context() + try: + verify_results = ctx.verify(message, signature)[1] + return verify_results.signatures + except gpg.errors.BadSignatures as e: + raise GPGProblem(str(e), code=GPGCode.BAD_SIGNATURE) + except gpg.errors.GPGMEError as e: + raise GPGProblem(e.message, code=e.getcode()) def decrypt_verify(encrypted): - '''Decrypts the given ciphertext string and returns both the + """Decrypts the given ciphertext string and returns both the signatures (if any) and the plaintext. - :param encrypted: the mail to decrypt - :returns: a tuple (sigs, plaintext) with sigs being a list of a - :class:`gpgme.Signature` and plaintext is a `str` holding - the decrypted mail + :param str encrypted: the mail to decrypt + :returns: the signatures and decrypted plaintext data + :rtype: tuple[list[gpg.resuit.Signature], str] :raises: :class:`~alot.errors.GPGProblem` if the decryption fails - ''' - encrypted_data = StringIO(encrypted) - plaintext_data = StringIO() - ctx = gpgme.Context() - try: - sigs = ctx.decrypt_verify(encrypted_data, plaintext_data) - except gpgme.GpgmeError as e: - raise GPGProblem(e.message, code=e.code) - - plaintext_data.seek(0, os.SEEK_SET) - return sigs, plaintext_data.read() - - -def hash_key(key): """ - Returns a hash of the given key. This is a workaround for - https://bugs.launchpad.net/pygpgme/+bug/1089865 - and can be removed if the missing feature is added to pygpgme. + ctx = gpg.core.Context() + try: + (plaintext, _, verify_result) = ctx.decrypt(encrypted, verify=True) + except gpg.errors.GPGMEError as e: + raise GPGProblem(e.message, code=e.getcode()) + # what if the signature is bad? - :param key: the key we want a hash of - :type key: gpgme.Key - :returns: a hash of the key - :rtype: str - """ - hash_str = "" - for tmp_key in key.subkeys: - hash_str += tmp_key.keyid - return hash_str + return verify_result.signatures, plaintext def validate_key(key, sign=False, encrypt=False): @@ -253,12 +223,16 @@ def validate_key(key, sign=False, encrypt=False): signing or encrypting. Raise GPGProblem otherwise. :param key: the GPG key to check - :type key: gpgme.Key + :type key: gpg.gpgme._gpgme_key :param sign: whether the key should be able to sign :type sign: bool :param encrypt: whether the key should be able to encrypt :type encrypt: bool - + :raises ~alot.errors.GPGProblem: If the key is revoked, expired, or invalid + :raises ~alot.errors.GPGProblem: If encrypt is true and the key cannot be + used to encrypt + :raises ~alot.errors.GPGProblem: If sign is true and th key cannot be used + to encrypt """ if key.revoked: raise GPGProblem('The key "{}" is revoked.'.format(key.uids[0].uid), @@ -285,16 +259,16 @@ def check_uid_validity(key, email): email is assumed to belong to the key. :param key: the GPG key to which the email should belong - :type key: gpgme.Key + :type key: gpg.gpgme._gpgme_key :param email: the email address that should belong to the key :type email: str :returns: whether the key can be assumed to belong to the given email :rtype: bool - """ - for key_uid in key.uids: - if email == key_uid.email and not key_uid.revoked and \ - not key_uid.invalid and \ - key_uid.validity >= gpgme.VALIDITY_FULL: - return True - return False + def check(key_uid): + return (email == key_uid.email and + not key_uid.revoked and + not key_uid.invalid and + key_uid.validity >= gpg.constants.validity.FULL) + + return any(check(u) for u in key.uids) diff --git a/alot/db/envelope.py b/alot/db/envelope.py index 0a6d4f58..3dc12278 100644 --- a/alot/db/envelope.py +++ b/alot/db/envelope.py @@ -13,8 +13,7 @@ from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication import email.charset as charset - -import gpgme +import gpg from .attachment import Attachment from .utils import encode_header @@ -197,8 +196,8 @@ class Envelope(object): raise GPGProblem("Could not sign message (GPGME " "did not return a signature)", code=GPGCode.KEY_CANNOT_SIGN) - except gpgme.GpgmeError as e: - if e.code == gpgme.ERR_BAD_PASSPHRASE: + except gpg.errors.GPGMEError as e: + if e.getcode() == gpg.errors.BAD_PASSPHRASE: # If GPG_AGENT_INFO is unset or empty, the user just does # not have gpg-agent running (properly). if os.environ.get('GPG_AGENT_INFO', '').strip() == '': @@ -237,7 +236,7 @@ class Envelope(object): try: encrypted_str = crypto.encrypt(plaintext, self.encrypt_keys.values()) - except gpgme.GpgmeError as e: + except gpg.errors.GPGMEError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) outer_msg = MIMEMultipart('encrypted', diff --git a/alot/db/utils.py b/alot/db/utils.py index 3cff108a..994e85b2 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -35,7 +35,7 @@ def add_signature_headers(mail, sigs, error_msg): verification was successful. :param mail: :class:`email.message.Message` the message to entitle - :param sigs: list of :class:`gpgme.Signature` + :param sigs: list of :class:`gpg.results.Signature` :param error_msg: `str` containing an error message, the empty string indicating no error ''' diff --git a/alot/errors.py b/alot/errors.py index 9860f411..c312e674 100644 --- a/alot/errors.py +++ b/alot/errors.py @@ -13,6 +13,8 @@ class GPGCode(object): KEY_CANNOT_ENCRYPT = 7 KEY_CANNOT_SIGN = 8 INVALID_HASH = 9 + INVALID_HASH_ALGORITHM = 10 + BAD_SIGNATURE = 11 class GPGProblem(Exception): diff --git a/alot/utils/configobj.py b/alot/utils/configobj.py index aba61c3d..5e04409a 100644 --- a/alot/utils/configobj.py +++ b/alot/utils/configobj.py @@ -136,7 +136,7 @@ def force_list(value, min=None, max=None): def gpg_key(value): """ test if value points to a known gpg key - and return that key as :class:`pyme.pygpgme._gpgme_key`. + and return that key as a gpg key object. """ try: return crypto.get_key(value) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5ebab550..e096cc89 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,7 +33,7 @@ MOCK_MODULES = ['twisted', 'twisted.internet', 'urwid', 'urwidtrees', 'magic', - 'gpgme', + 'gpg', 'configobj', 'validate', 'argparse'] diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 2110315b..170c384b 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -13,7 +13,7 @@ A full list of dependencies is below: * `libnotmuch <http://notmuchmail.org/>`_ and it's python bindings, ≥ `0.13` * `urwid <http://excess.org/urwid/>`_ toolkit, ≥ `1.1.0` * `urwidtrees <https://github.com/pazz/urwidtrees>`_, ≥ `1.0` -* `PyGPGME <https://launchpad.net/pygpgme>`_ ≥ `0.2` +* `gpg <https://pypi.python.org/pypi/gpg>`_ .. note:: urwidtrees was only recently detached from alot and is not widely available as a separate package. You can install it e.g., via @@ -26,11 +26,11 @@ A full list of dependencies is below: On debian/ubuntu the rest are packaged as:: - python-setuptools python-magic python-configobj python-twisted python-notmuch python-urwid python-gpgme + python-setuptools python-magic python-configobj python-twisted python-notmuch python-urwid python-gpg On fedora/redhat these are packaged as:: - python-setuptools python-magic python-configobj python-twisted python-notmuch python-urwid pygpgme + python-setuptools python-magic python-configobj python-twisted python-notmuch python-urwid python-gpg Alot uses `mailcap <http://en.wikipedia.org/wiki/Mailcap>`_ to look up mime-handler for inline rendering and opening of attachments. For a full description of the maicap protocol consider the @@ -32,7 +32,7 @@ setup(name='alot', 'twisted>=10.2.0', 'python-magic', 'configobj>=4.7.0', - 'pygpgme>=0.2'], + 'gpg'], tests_require=[ 'mock', ], diff --git a/tests/crypto_test.py b/tests/crypto_test.py index e0e52cee..bd46adf2 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -10,7 +10,7 @@ import subprocess import tempfile import unittest -import gpgme +import gpg import mock from alot import crypto @@ -43,15 +43,13 @@ def setUpModule(): mock_home.start() MOD_CLEAN.add_cleanup(mock_home.stop) - ctx = gpgme.Context() - ctx.armor = True - - # Add the public and private keys. They have no password - search_dir = os.path.join(os.path.dirname(__file__), 'static/gpg-keys') - for each in os.listdir(search_dir): - if os.path.splitext(each)[1] == '.gpg': - with open(os.path.join(search_dir, each)) as f: - ctx.import_(f) + with gpg.core.Context(armor=True) as ctx: + # Add the public and private keys. They have no password + search_dir = os.path.join(os.path.dirname(__file__), 'static/gpg-keys') + for each in os.listdir(search_dir): + if os.path.splitext(each)[1] == '.gpg': + with open(os.path.join(search_dir, each)) as f: + ctx.op_import(f) @MOD_CLEAN.wrap_teardown @@ -66,29 +64,54 @@ def tearDownModule(): os.kill(int(pid), signal.SIGKILL) +def make_key(revoked=False, expired=False, invalid=False, can_encrypt=True, + can_sign=True): + # This is ugly + mock_key = mock.create_autospec(gpg._gpgme._gpgme_key) + mock_key.uids = [mock.Mock(uid=u'mocked')] + mock_key.revoked = revoked + mock_key.expired = expired + mock_key.invalid = invalid + mock_key.can_encrypt = can_encrypt + mock_key.can_sign = can_sign + + return mock_key + + +def make_uid(email, revoked=False, invalid=False, + validity=gpg.constants.validity.FULL): + uid = mock.Mock() + uid.email = email + uid.revoked = revoked + uid.invalid = invalid + uid.validity = validity + + return uid + + class TestHashAlgorithmHelper(unittest.TestCase): """Test cases for the helper function RFC3156_canonicalize.""" def test_returned_string_starts_with_pgp(self): - result = crypto.RFC3156_micalg_from_algo(gpgme.MD_MD5) + result = crypto.RFC3156_micalg_from_algo(gpg.constants.md.MD5) self.assertTrue(result.startswith('pgp-')) def test_returned_string_is_lower_case(self): - result = crypto.RFC3156_micalg_from_algo(gpgme.MD_MD5) + result = crypto.RFC3156_micalg_from_algo(gpg.constants.md.MD5) self.assertTrue(result.islower()) def test_raises_for_unknown_hash_name(self): with self.assertRaises(GPGProblem): - crypto.RFC3156_micalg_from_algo(gpgme.MD_NONE) + crypto.RFC3156_micalg_from_algo(gpg.constants.md.NONE) class TestDetachedSignatureFor(unittest.TestCase): def test_valid_signature_generated(self): - ctx = gpgme.Context() to_sign = "this is some text.\nit is more than nothing.\n" - _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) + with gpg.core.Context() as ctx: + _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) with tempfile.NamedTemporaryFile(delete=False) as f: f.write(detached) @@ -108,9 +131,9 @@ class TestDetachedSignatureFor(unittest.TestCase): class TestVerifyDetached(unittest.TestCase): def test_verify_signature_good(self): - ctx = gpgme.Context() to_sign = "this is some text.\nIt's something\n." - _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) + with gpg.core.Context() as ctx: + _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) try: crypto.verify_detached(to_sign, detached) @@ -118,10 +141,10 @@ class TestVerifyDetached(unittest.TestCase): raise AssertionError def test_verify_signature_bad(self): - ctx = gpgme.Context() to_sign = "this is some text.\nIt's something\n." similar = "this is some text.\r\n.It's something\r\n." - _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) + with gpg.core.Context() as ctx: + _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR)) with self.assertRaises(GPGProblem): crypto.verify_detached(similar, detached) @@ -218,7 +241,7 @@ class TestCheckUIDValidity(unittest.TestCase): key = utilities.make_key() key.uids[0] = utilities.make_uid( mock.sentinel.EMAIL, - validity=gpgme.VALIDITY_UNDEFINED) + validity=gpg.constants.validity.UNDEFINED) ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL) self.assertFalse(ret) @@ -236,26 +259,41 @@ class TestListKeys(unittest.TestCase): values = crypto.list_keys(hint="ambig") self.assertEqual(len(list(values)), 2) + def test_list_keys_pub(self): + values = list(crypto.list_keys(hint="ambigu"))[0] + self.assertEqual(values.uids[0].email, u'amigbu@example.com') + self.assertFalse(values.secret) + + def test_list_keys_private(self): + values = list(crypto.list_keys(hint="ambigu", private=True))[0] + self.assertEqual(values.uids[0].email, u'amigbu@example.com') + self.assertTrue(values.secret) + class TestGetKey(unittest.TestCase): def test_plain(self): # Test the uid of the only identity attached to the key we generated. - ctx = gpgme.Context() - expected = ctx.get_key(FPR).uids[0].uid + with gpg.core.Context() as ctx: + expected = ctx.get_key(FPR).uids[0].uid actual = crypto.get_key(FPR).uids[0].uid self.assertEqual(expected, actual) def test_validate(self): # Since we already test validation we're only going to test validate # once. - ctx = gpgme.Context() - expected = ctx.get_key(FPR).uids[0].uid + with gpg.core.Context() as ctx: + expected = ctx.get_key(FPR).uids[0].uid actual = crypto.get_key(FPR, validate=True, encrypt=True, sign=True).uids[0].uid self.assertEqual(expected, actual) def test_missing_key(self): with self.assertRaises(GPGProblem) as caught: + crypto.get_key('foo@example.com') + self.assertEqual(caught.exception.code, GPGCode.NOT_FOUND) + + def test_invalid_key(self): + with self.assertRaises(GPGProblem) as caught: crypto.get_key('z') self.assertEqual(caught.exception.code, GPGCode.NOT_FOUND) @@ -274,10 +312,15 @@ class TestGetKey(unittest.TestCase): @staticmethod def _context_mock(): - error = gpgme.GpgmeError() - error.code = gpgme.ERR_AMBIGUOUS_NAME + class CustomError(gpg.errors.GPGMEError): + """A custom GPGMEError class that always has an errors code of + AMBIGUOUS_NAME. + """ + def getcode(self): + return gpg.errors.AMBIGUOUS_NAME + context_mock = mock.Mock() - context_mock.get_key = mock.Mock(side_effect=error) + context_mock.get_key = mock.Mock(side_effect=CustomError) return context_mock @@ -285,7 +328,7 @@ class TestGetKey(unittest.TestCase): invalid_key = utilities.make_key(invalid=True) valid_key = utilities.make_key() - with mock.patch('alot.crypto.gpgme.Context', + with mock.patch('alot.crypto.gpg.core.Context', mock.Mock(return_value=self._context_mock())), \ mock.patch('alot.crypto.list_keys', mock.Mock(return_value=[valid_key, invalid_key])): @@ -293,7 +336,7 @@ class TestGetKey(unittest.TestCase): self.assertIs(key, valid_key) def test_ambiguous_two_valid(self): - with mock.patch('alot.crypto.gpgme.Context', + with mock.patch('alot.crypto.gpg.core.Context', mock.Mock(return_value=self._context_mock())), \ mock.patch('alot.crypto.list_keys', mock.Mock(return_value=[utilities.make_key(), @@ -303,7 +346,7 @@ class TestGetKey(unittest.TestCase): self.assertEqual(cm.exception.code, GPGCode.AMBIGUOUS_NAME) def test_ambiguous_no_valid(self): - with mock.patch('alot.crypto.gpgme.Context', + with mock.patch('alot.crypto.gpg.core.Context', mock.Mock(return_value=self._context_mock())), \ mock.patch('alot.crypto.list_keys', mock.Mock(return_value=[ @@ -337,3 +380,5 @@ class TestDecrypt(unittest.TestCase): encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)]) _, dec = crypto.decrypt_verify(encrypted) self.assertEqual(to_encrypt, dec) + + # TODO: test for "combined" method diff --git a/tests/utilities.py b/tests/utilities.py index 45433c99..402feb38 100644 --- a/tests/utilities.py +++ b/tests/utilities.py @@ -21,7 +21,7 @@ from __future__ import absolute_import import functools import unittest -import gpgme +import gpg import mock @@ -149,7 +149,7 @@ class ModuleCleanup(object): def make_uid(email, uid=u'mocked', revoked=False, invalid=False, - validity=gpgme.VALIDITY_FULL): + validity=gpg.constants.validity.FULL): uid_ = mock.Mock() uid_.email = email uid_.uid = uid @@ -162,7 +162,7 @@ def make_uid(email, uid=u'mocked', revoked=False, invalid=False, def make_key(revoked=False, expired=False, invalid=False, can_encrypt=True, can_sign=True): - mock_key = mock.create_autospec(gpgme.Key) + mock_key = mock.Mock() mock_key.uids = [make_uid(u'foo@example.com')] mock_key.revoked = revoked mock_key.expired = expired |