summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDylan Baker <dylan@pnwbakers.com>2017-07-11 16:36:22 -0700
committerDylan Baker <dylan@pnwbakers.com>2017-07-15 15:01:51 -0700
commitb40e654617ee8bb4a81b07a70198ae48548f2f05 (patch)
treebf3e7ad43bd1b05348413aecf522c77da9cf78c1
parent5977cf3f43715fe0c22b14d861d6912d4b571c26 (diff)
tests/crypto: Add tests for the crypto module
This covers mosts of the functions in the crypto module, but doesn't bother with the hash_key function, which is slated for deletion in the port to python-gpg. It also doesn't cover re-raising errors.
-rw-r--r--tests/crypto_test.py328
-rw-r--r--tests/static/gpg-keys/ambig1-pub.gpg30
-rw-r--r--tests/static/gpg-keys/ambig1-sec.gpg57
-rw-r--r--tests/static/gpg-keys/ambig2-pub.gpgbin0 -> 1235 bytes
-rw-r--r--tests/static/gpg-keys/ambig2-sec.gpgbin0 -> 2536 bytes
-rw-r--r--tests/static/gpg-keys/pub.gpg (renamed from tests/static/pub.gpg)0
-rw-r--r--tests/static/gpg-keys/sec.gpg (renamed from tests/static/sec.gpg)0
-rw-r--r--tests/utilities.py45
8 files changed, 427 insertions, 33 deletions
diff --git a/tests/crypto_test.py b/tests/crypto_test.py
index 51cb48ba..506d5868 100644
--- a/tests/crypto_test.py
+++ b/tests/crypto_test.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import
import os
import shutil
+import signal
import subprocess
import tempfile
import unittest
@@ -12,10 +13,80 @@ import unittest
import gpgme
import mock
+from alot import crypto
+from alot.errors import GPGProblem, GPGCode
+
from . import utilities
-from alot import crypto
-from alot.errors import GPGProblem
+
+MOD_CLEAN = utilities.ModuleCleanup()
+
+# A useful single fingerprint for tests that only care about one key. This
+# key will not be ambiguous
+FPR = "F74091D4133F87D56B5D343C1974EC55FBC2D660"
+
+# Some additional keys, these keys may be ambigiuos
+EXTRA_FPRS = [
+ "DD19862809A7573A74058FF255937AFBB156245D",
+ "2071E9C8DB4EF5466F4D233CF730DF92C4566CE7",
+]
+
+DEVNULL = open('/dev/null', 'w')
+MOD_CLEAN.add_cleanup(DEVNULL.close)
+
+
+@MOD_CLEAN.wrap_setup
+def setUpModule():
+ home = tempfile.mkdtemp()
+ MOD_CLEAN.add_cleanup(shutil.rmtree, home)
+ mock_home = mock.patch.dict(os.environ, {'GNUPGHOME': home})
+ 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)
+
+
+@MOD_CLEAN.wrap_teardown
+def tearDownModule():
+ # Kill any gpg-agent's that have been opened
+ lookfor = 'gpg-agent --homedir {}'.format(os.environ['GNUPGHOME'])
+
+ out = subprocess.check_output(['ps', 'xo', 'pid,cmd'], stderr=DEVNULL)
+ for each in out.strip().split('\n'):
+ pid, cmd = each.strip().split(' ', 1)
+ if cmd.startswith(lookfor):
+ os.kill(int(pid), signal.SIGKILL)
+
+
+def make_key(revoked=False, expired=False, invalid=False, can_encrypt=True,
+ can_sign=True):
+ mock_key = mock.create_autospec(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=gpgme.VALIDITY_FULL):
+ uid = mock.Mock()
+ uid.email = email
+ uid.revoked = revoked
+ uid.invalid = invalid
+ uid.validity = validity
+
+ return uid
class TestHashAlgorithmHelper(unittest.TestCase):
@@ -35,36 +106,12 @@ class TestHashAlgorithmHelper(unittest.TestCase):
crypto.RFC3156_micalg_from_algo(gpgme.MD_NONE)
-class TestSignature(utilities.TestCaseClassCleanup):
-
- FPR = "F74091D4133F87D56B5D343C1974EC55FBC2D660"
+class TestDetachedSignatureFor(unittest.TestCase):
- @classmethod
- def setUpClass(cls):
- # Create a temporary directory to use as gnupg's home directory. This
- # allows us to import keys without dirtying the user's keyring
- home = tempfile.mkdtemp()
- cls.addClassCleanup(shutil.rmtree, home)
- mock_home = mock.patch.dict(os.environ, {'GNUPGHOME': home})
- mock_home.start()
- cls.addClassCleanup(mock_home.stop)
-
- # create a single context to use class wide.
- cls.ctx = gpgme.Context()
- cls.ctx.armor = True
-
- # Add the public and private keys. They have no password
- here = os.path.dirname(__file__)
- with open(os.path.join(here, 'static/pub.gpg')) as f:
- cls.ctx.import_(f)
- with open(os.path.join(here, 'static/sec.gpg')) as f:
- cls.ctx.import_(f)
-
- cls.key = cls.ctx.get_key(cls.FPR)
-
- def test_detached_signature_for(self):
+ 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, self.key)
+ _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR))
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(detached)
@@ -76,12 +123,17 @@ class TestSignature(utilities.TestCaseClassCleanup):
text = f.name
self.addCleanup(os.unlink, f.name)
- res = subprocess.check_call(['gpg', '--verify', sig, text])
+ res = subprocess.check_call(['gpg', '--verify', sig, text],
+ stdout=DEVNULL, stderr=DEVNULL)
self.assertEqual(res, 0)
+
+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, self.key)
+ _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR))
try:
crypto.verify_detached(to_sign, detached)
@@ -89,9 +141,219 @@ class TestSignature(utilities.TestCaseClassCleanup):
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, self.key)
+ _, detached = crypto.detached_signature_for(to_sign, ctx.get_key(FPR))
with self.assertRaises(GPGProblem):
crypto.verify_detached(similar, detached)
+
+
+class TestValidateKey(unittest.TestCase):
+
+ def test_valid(self):
+ try:
+ crypto.validate_key(make_key())
+ except GPGProblem as e:
+ raise AssertionError(e)
+
+ def test_revoked(self):
+ with self.assertRaises(GPGProblem) as caught:
+ crypto.validate_key(make_key(revoked=True))
+
+ self.assertEqual(caught.exception.code, GPGCode.KEY_REVOKED)
+
+ def test_expired(self):
+ with self.assertRaises(GPGProblem) as caught:
+ crypto.validate_key(make_key(expired=True))
+
+ self.assertEqual(caught.exception.code, GPGCode.KEY_EXPIRED)
+
+ def test_invalid(self):
+ with self.assertRaises(GPGProblem) as caught:
+ crypto.validate_key(make_key(invalid=True))
+
+ self.assertEqual(caught.exception.code, GPGCode.KEY_INVALID)
+
+ def test_encrypt(self):
+ with self.assertRaises(GPGProblem) as caught:
+ crypto.validate_key(make_key(can_encrypt=False), encrypt=True)
+
+ self.assertEqual(caught.exception.code, GPGCode.KEY_CANNOT_ENCRYPT)
+
+ def test_encrypt_no_check(self):
+ try:
+ crypto.validate_key(make_key(can_encrypt=False))
+ except GPGProblem as e:
+ raise AssertionError(e)
+
+ def test_sign(self):
+ with self.assertRaises(GPGProblem) as caught:
+ crypto.validate_key(make_key(can_sign=False), sign=True)
+
+ self.assertEqual(caught.exception.code, GPGCode.KEY_CANNOT_SIGN)
+
+ def test_sign_no_check(self):
+ try:
+ crypto.validate_key(make_key(can_sign=False))
+ except GPGProblem as e:
+ raise AssertionError(e)
+
+
+class TestCheckUIDValidity(unittest.TestCase):
+
+ def test_valid_single(self):
+ key = make_key()
+ key.uids[0] = make_uid(mock.sentinel.EMAIL)
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL)
+ self.assertTrue(ret)
+
+ def test_valid_multiple(self):
+ key = make_key()
+ key.uids = [
+ make_uid(mock.sentinel.EMAIL),
+ make_uid(mock.sentinel.EMAIL1),
+ ]
+
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL1)
+ self.assertTrue(ret)
+
+ def test_invalid_email(self):
+ key = make_key()
+ key.uids[0] = make_uid(mock.sentinel.EMAIL)
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL1)
+ self.assertFalse(ret)
+
+ def test_invalid_revoked(self):
+ key = make_key()
+ key.uids[0] = make_uid(mock.sentinel.EMAIL, revoked=True)
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL)
+ self.assertFalse(ret)
+
+ def test_invalid_invalid(self):
+ key = make_key()
+ key.uids[0] = make_uid(mock.sentinel.EMAIL, invalid=True)
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL)
+ self.assertFalse(ret)
+
+ def test_invalid_not_enough_trust(self):
+ key = make_key()
+ key.uids[0] = make_uid(mock.sentinel.EMAIL,
+ validity=gpgme.VALIDITY_UNDEFINED)
+ ret = crypto.check_uid_validity(key, mock.sentinel.EMAIL)
+ self.assertFalse(ret)
+
+
+class TestListKeys(unittest.TestCase):
+
+ def test_list_no_hints(self):
+ # This only tests that you get 3 keys back (the number in our test
+ # keyring), it might be worth adding tests to check more about the keys
+ # returned
+ values = crypto.list_keys()
+ self.assertEqual(len(list(values)), 3)
+
+ def test_list_hint(self):
+ values = crypto.list_keys(hint="ambig")
+ self.assertEqual(len(list(values)), 2)
+
+
+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
+ 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
+ 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('z')
+ self.assertEqual(caught.exception.code, GPGCode.NOT_FOUND)
+
+ @mock.patch('alot.crypto.check_uid_validity', mock.Mock(return_value=True))
+ def test_signed_only_true(self):
+ try:
+ crypto.get_key(FPR, signed_only=True)
+ except GPGProblem as e:
+ raise AssertionError(e)
+
+ @mock.patch('alot.crypto.check_uid_validity', mock.Mock(return_value=False))
+ def test_signed_only_false(self):
+ with self.assertRaises(GPGProblem) as e:
+ crypto.get_key(FPR, signed_only=True)
+ self.assertEqual(e.exception.code, GPGCode.NOT_FOUND)
+
+ @staticmethod
+ def _context_mock():
+ error = gpgme.GpgmeError()
+ error.code = gpgme.ERR_AMBIGUOUS_NAME
+ context_mock = mock.Mock()
+ context_mock.get_key = mock.Mock(side_effect=error)
+
+ return context_mock
+
+ def test_ambiguous_one_valid(self):
+ invalid_key = make_key(invalid=True)
+ valid_key = make_key()
+
+ with mock.patch('alot.crypto.gpgme.Context',
+ mock.Mock(return_value=self._context_mock())), \
+ mock.patch('alot.crypto.list_keys',
+ mock.Mock(return_value=[valid_key, invalid_key])):
+ key = crypto.get_key('placeholder')
+ self.assertIs(key, valid_key)
+
+ def test_ambiguous_two_valid(self):
+ with mock.patch('alot.crypto.gpgme.Context',
+ mock.Mock(return_value=self._context_mock())), \
+ mock.patch('alot.crypto.list_keys',
+ mock.Mock(return_value=[make_key(), make_key()])):
+ with self.assertRaises(crypto.GPGProblem) as cm:
+ crypto.get_key('placeholder')
+ self.assertEqual(cm.exception.code, GPGCode.AMBIGUOUS_NAME)
+
+ def test_ambiguous_no_valid(self):
+ with mock.patch('alot.crypto.gpgme.Context',
+ mock.Mock(return_value=self._context_mock())), \
+ mock.patch('alot.crypto.list_keys',
+ mock.Mock(return_value=[make_key(invalid=True),
+ make_key(invalid=True)])):
+ with self.assertRaises(crypto.GPGProblem) as cm:
+ crypto.get_key('placeholder')
+ self.assertEqual(cm.exception.code, GPGCode.NOT_FOUND)
+
+
+class TestEncrypt(unittest.TestCase):
+
+ def test_encrypt(self):
+ to_encrypt = "this is a string\nof data."
+ encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)])
+
+ with tempfile.NamedTemporaryFile(delete=False) as f:
+ f.write(encrypted)
+ enc_file = f.name
+ self.addCleanup(os.unlink, enc_file)
+
+ dec = subprocess.check_output(['gpg', '--decrypt', enc_file],
+ stderr=DEVNULL)
+ self.assertEqual(to_encrypt, dec)
+
+
+class TestDecrypt(unittest.TestCase):
+
+ def test_decrypt(self):
+ to_encrypt = "this is a string\nof data."
+ encrypted = crypto.encrypt(to_encrypt, keys=[crypto.get_key(FPR)])
+ _, dec = crypto.decrypt_verify(encrypted)
+ self.assertEqual(to_encrypt, dec)
diff --git a/tests/static/gpg-keys/ambig1-pub.gpg b/tests/static/gpg-keys/ambig1-pub.gpg
new file mode 100644
index 00000000..8bc893d6
--- /dev/null
+++ b/tests/static/gpg-keys/ambig1-pub.gpg
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFlmXXwBCACip8btluktWTxaUdyDLpBS2GrXgDe1M/zT0TMaNN8p0r0oUnNf
+a7dSHb4QLbaY31d3ftt5IoaSXENnJP2WREujQdQ9SlXb4sVNo6W37t0NtKGn9kqp
+T2ajgXj1lJ+ZiULHRlSmBoA2blFeABE4PRgef+x6aDJpMtODWG/2NaWw/gFn6kqS
+OGyqMp0nM3OHeEwZAjf+n1f07wqJHK+m1V3I2rY4wm5LST0kZXJGYFDfjaTuTOOC
+yPMyhWoqJ/CCWavO47MRdYrlM6qUbVBTQ8DSBGZO2yuF/ILLICC8d/ODGva+kNDq
+Bm4PmYvVrB0osfxMXVBaxezwOKkDiE3w4fOXABEBAAG0GWFtYmlnIDxhbWJpZ0Bl
+eGFtcGxlLmNvbT6JAVQEEwEIAD4WIQTdGYYoCadXOnQFj/JVk3r7sVYkXQUCWWZd
+fAIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRBVk3r7sVYkXXte
+B/sGzTrrHk7da9GqLtJPYPiI4uckInb7xcxOANd2MMO27ZdBcy+77tdVNDmGnmUP
+W1KW0APTL5JL8XNo39r7eKGfp66g6qF8Lto/X22DIyv5tld1XARwXt5z19P/oA9F
+BTwSkqP/pWg5rYJwSmZwNdYF+kFTTokTxZgXDVfQcDa4qnPRy4wZSoiQewE3itdn
+fwexxuhXmS406UPLBhmFItGH4BThk0z5b0EVLt1zvff6q1po3hlcZr+2u71WvYzF
+ufJfWQr5xo6PX1Po16Da5Pz+mnuaz7vT+i3RwLo1wOsu7rXNAAVzg3VstGKAobGE
+ix+ET7uB05xJLFLt6jlVg2ChuQENBFlmXXwBCADLAS1JN3UykZbL3JCiatPe6Ce6
+ErkzEwnECJxTyg2UqsrQJ/SdPRCJ8wyQ0jWBezn/4MNCiJoacPR+YVo6CVi/R/Kc
+7qfiqxVp5mfxSf4qpbC1esZ0L20VdhUnWKc+YvSUhGPIe73ruiDVXt+QnyZiLm9B
+niUNaEL/j8GI9o5J6y2v3IQJwO9cmVKuQa2aE1c9zG4ZxIrzlgrI8bF2jcm/olx6
+a1X55TsqQEA/CEl5tsyr5gOBa/4qXc1STUCZthEKffbDsSH+8d+26Y7Qw201BWWQ
+6Hx/8jnyaKzmP+ANymG2Bj4lioEY96Qu6vzzQ4RwUvkcJB6K1Osr/diwhkeJABEB
+AAGJATwEGAEIACYWIQTdGYYoCadXOnQFj/JVk3r7sVYkXQUCWWZdfAIbDAUJA8Jn
+AAAKCRBVk3r7sVYkXY01CACDoMkoAYNeCa43g11zywczl4ZyGLQuDD1BomKP3Jur
+58sNo6OjYd0bRSUtn6V7vPKaMIm1b+VF2L9/c0Oia807yr+7h36XoriTas1iAqkp
+gx3ZQ06daenq/074edPvnrknzYpnzCZM5SQcyCTU6QPgLU2rNZAl75CH2SfpY0SK
+HUMHjiD+HKueNrAstqsY1DJdO+aQDdYxLJxuax2003+qriX0hDp0X7Nym4v+cd1X
+xgsVMkqFIYkwJXUlooMRLAb1ifkUEiezsr5i5iudwrsIb7WjcDEXOFSHRXpT63z0
+glpUupwfhHcODTOmIgyhrfbmN60eiEcojfSdMsWi2ktr
+=sqEe
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/tests/static/gpg-keys/ambig1-sec.gpg b/tests/static/gpg-keys/ambig1-sec.gpg
new file mode 100644
index 00000000..35be3bcb
--- /dev/null
+++ b/tests/static/gpg-keys/ambig1-sec.gpg
@@ -0,0 +1,57 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+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
+=r1WA
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/tests/static/gpg-keys/ambig2-pub.gpg b/tests/static/gpg-keys/ambig2-pub.gpg
new file mode 100644
index 00000000..e52e2f62
--- /dev/null
+++ b/tests/static/gpg-keys/ambig2-pub.gpg
Binary files differ
diff --git a/tests/static/gpg-keys/ambig2-sec.gpg b/tests/static/gpg-keys/ambig2-sec.gpg
new file mode 100644
index 00000000..a7158131
--- /dev/null
+++ b/tests/static/gpg-keys/ambig2-sec.gpg
Binary files differ
diff --git a/tests/static/pub.gpg b/tests/static/gpg-keys/pub.gpg
index c94f951d..c94f951d 100644
--- a/tests/static/pub.gpg
+++ b/tests/static/gpg-keys/pub.gpg
diff --git a/tests/static/sec.gpg b/tests/static/gpg-keys/sec.gpg
index f00b017f..f00b017f 100644
--- a/tests/static/sec.gpg
+++ b/tests/static/gpg-keys/sec.gpg
diff --git a/tests/utilities.py b/tests/utilities.py
index aae79552..ed169b59 100644
--- a/tests/utilities.py
+++ b/tests/utilities.py
@@ -98,3 +98,48 @@ class TestCaseClassCleanup(unittest.TestCase):
# TODO: addCleanups success if part of the success of the test,
# what should we do here?
func(*args, **kwargs)
+
+
+class ModuleCleanup(object):
+ """Class for managing module level setup and teardown fixtures.
+
+ Because of the way unittest is implemented it's rather difficult to write
+ elegent fixtures because setUpModule and tearDownModule must exist when the
+ module is initialized, so you can't do things like assign the methods to
+ setUpModule and tearDownModule, nor can you just do some globals
+ manipulation.
+ """
+
+ def __init__(self):
+ self.__stack = []
+
+ def do_cleanups(self):
+ while self.__stack:
+ func, args, kwargs = self.__stack.pop()
+ func(*args, **kwargs)
+
+ def add_cleanup(self, func, *args, **kwargs):
+ self.__stack.append((func, args, kwargs))
+
+ def wrap_teardown(self, teardown):
+
+ @functools.wraps(teardown)
+ def wrapper():
+ try:
+ teardown()
+ finally:
+ self.do_cleanups()
+
+ return wrapper
+
+ def wrap_setup(self, setup):
+
+ @functools.wraps(setup)
+ def wrapper():
+ try:
+ setup()
+ except Exception:
+ self.do_cleanups()
+ raise
+
+ return wrapper