diff options
author | Dylan Baker <baker.dylan.c@gmail.com> | 2017-07-12 14:25:12 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-12 14:25:12 -0700 |
commit | 5977cf3f43715fe0c22b14d861d6912d4b571c26 (patch) | |
tree | eeb0220a8f7a73500d38e525107df4447bd5b837 | |
parent | 62224402d278c2ce6e2f2689262cb6399a05aac0 (diff) | |
parent | 9f9d4639a68245555b6aa777a0269b45dce5035f (diff) |
Merge pull request #1078 from dcbaker/wip/fix-error-handling-of-signed
crypto: Fix handling of signed messages
-rw-r--r-- | alot/crypto.py | 9 | ||||
-rw-r--r-- | alot/db/utils.py | 5 | ||||
-rw-r--r-- | tests/crypto_test.py | 69 | ||||
-rw-r--r-- | tests/static/pub.gpg | 30 | ||||
-rw-r--r-- | tests/static/sec.gpg | 57 | ||||
-rw-r--r-- | tests/utilities.py | 100 |
6 files changed, 264 insertions, 6 deletions
diff --git a/alot/crypto.py b/alot/crypto.py index 08213f34..b0849ae2 100644 --- a/alot/crypto.py +++ b/alot/crypto.py @@ -202,10 +202,11 @@ def verify_detached(message, signature): message_data = StringIO(message) signature_data = StringIO(signature) ctx = gpgme.Context() - try: - return ctx.verify(signature_data, message_data, None) - except gpgme.GpgmeError as e: - raise GPGProblem(e.message, code=e.code) + + 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 def decrypt_verify(encrypted): diff --git a/alot/db/utils.py b/alot/db/utils.py index 2bcf6aca..ac91098a 100644 --- a/alot/db/utils.py +++ b/alot/db/utils.py @@ -140,8 +140,9 @@ def message_from_file(handle): sigs = [] if not malformed: try: - sigs = crypto.verify_detached(m.get_payload(0).as_string(), - m.get_payload(1).get_payload()) + sigs = crypto.verify_detached( + helper.email_as_string(m.get_payload(0)), + m.get_payload(1).get_payload()) except GPGProblem as e: malformed = unicode(e) diff --git a/tests/crypto_test.py b/tests/crypto_test.py index 156d4ad6..51cb48ba 100644 --- a/tests/crypto_test.py +++ b/tests/crypto_test.py @@ -3,9 +3,16 @@ # For further details see the COPYING file from __future__ import absolute_import +import os +import shutil +import subprocess +import tempfile import unittest import gpgme +import mock + +from . import utilities from alot import crypto from alot.errors import GPGProblem @@ -26,3 +33,65 @@ class TestHashAlgorithmHelper(unittest.TestCase): def test_raises_for_unknown_hash_name(self): with self.assertRaises(GPGProblem): crypto.RFC3156_micalg_from_algo(gpgme.MD_NONE) + + +class TestSignature(utilities.TestCaseClassCleanup): + + FPR = "F74091D4133F87D56B5D343C1974EC55FBC2D660" + + @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): + to_sign = "this is some text.\nit is more than nothing.\n" + _, detached = crypto.detached_signature_for(to_sign, self.key) + + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(detached) + sig = f.name + self.addCleanup(os.unlink, f.name) + + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(to_sign) + text = f.name + self.addCleanup(os.unlink, f.name) + + res = subprocess.check_call(['gpg', '--verify', sig, text]) + self.assertEqual(res, 0) + + def test_verify_signature_good(self): + to_sign = "this is some text.\nIt's something\n." + _, detached = crypto.detached_signature_for(to_sign, self.key) + + try: + crypto.verify_detached(to_sign, detached) + except GPGProblem: + raise AssertionError + + def test_verify_signature_bad(self): + 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) + + with self.assertRaises(GPGProblem): + crypto.verify_detached(similar, detached) diff --git a/tests/static/pub.gpg b/tests/static/pub.gpg new file mode 100644 index 00000000..c94f951d --- /dev/null +++ b/tests/static/pub.gpg @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFllJaEBCACjvX5b8EkZI9b8uJnhjVwF3GKd/EJWnGYSLm5yQ1K7EKlAW8VK +yXM63iIqNbWTOzp52RtuNXFilXOabH3yZstBF8yn65uGm66CIdCJ8hOYZNhXVQGv +27aZ7dwwBxZNd4WCrskvD7Ll1DZZR+j1bMJSDLIh1i3OhR0uKUpTPT1Cp/XmTfII +wL3oh57PyNqlS6WXwkAVO8ZrRJQJP76ReSfvLKPaRwvPyuH0HD2A8U6gzbh+qVZl +UbDOwkka7C8yJV29805dT03bNPP0FKvo53xlzHVquO58AlCnhx3xT2+mg2TEwp4N +/Kz5E3PIm2njZLUS029vw83NNu04nRomzcO7ABEBAAG0HHRlc3QgdXNlciA8dGVz +dEBleGFtcGxlLmNvbT6JAVQEEwEIAD4WIQT3QJHUEz+H1WtdNDwZdOxV+8LWYAUC +WWUloQIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRAZdOxV+8LW +YDSiB/wNRzFSYRVOEhYYvgXCs14ca6HR5kxt07ei7ztp8D2rg8jxA+8pEzCtTnZb +GYZ4tnKCenKuRCsfxBI5pY/mwVT9TFfOY9umDyq471cn0Qx6Tt3lj1KhAJNnhtLJ +a/FQ9vPLQ4gghDeagw7FiS92xDsWsfkAouYAdBuUGgR8lu+vRHNCh/qh7KiBqtQX +wfGJb+ZCq4iXfFsU20TLZ0LZdIf1nRK1QWATI4d93t0zimvUVLSxK/A7xiwG0W+i +4SRGYeIy5ImCTa4gIFDw3C7fFeyhI4pzWz5I7iAKO6WJ6n+wMzi8aJMkApHZUbKY +HosXHR/IesA+wpBMLzc56N/NcwPsuQENBFllJaEBCADPqqyFwZVgvHPwG1GOPM5O +py2zb4hsgryOzNhddzVHzGD5pKyJsJiHrS+wOkIXMxvCfx0d1DL4vkwwsRMJND+0 +7T2fFCQXYqsGq36vyf/XgofUYrZVTQaBqRJ1IubD6n1kM+OljPLXyT2BhRuJbP40 +LFTBminndrMg55AkI4Sh8t/TIgwZtaxmzYrb94CNoK8Q/pEzFhMVe+0XlbyXn1x1 +fxnnmuHUeZP8lC9jdVIFBxYnGK3vcG0xmtHjE5wYiQ38+/IS+nn/zrs6OWpXF+L6 +UQE/53N0dG+tSWQ3kDv6HyAZCJRR2npkN4n0Ngq3Wx0fRgg0BnyZV2DsNeu1Mj3F +ABEBAAGJATwEGAEIACYWIQT3QJHUEz+H1WtdNDwZdOxV+8LWYAUCWWUloQIbDAUJ +A8JnAAAKCRAZdOxV+8LWYE0oB/9wLNmdC0psG3kMDWT9JBqo2Js/VXXBcjiRbErk +5uX0/FIqaM/HiP6AeXVp6w1NBOoQCH7tQKJ8Q0r4yh7Gi9W/qzlajFAAFVMtufq3 +2D7qy73NRwhAqFJeJVco/iRjEBzTDi48wtfxhFQDRAaqwDKoasNBxkhBk3fENaEc +kk8H03CfUx42gk7MBRt5jFIlN+/cNXxrdcz6XeGAPLOoqE+il+5+C/7slNMuDAV/ +6s/1DsYZ7C+jOycM6lMXaAkdveueXtOq9yaytmpjIysfmZIaXyahBxbRMvQtBAAR +eHTTrfhhS03FwT0YdioRcxNAHGW+Iltjrq3eygCmAilEBIAb +=DLcu +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/static/sec.gpg b/tests/static/sec.gpg new file mode 100644 index 00000000..f00b017f --- /dev/null +++ b/tests/static/sec.gpg @@ -0,0 +1,57 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=T5TK +-----END PGP PRIVATE KEY BLOCK----- diff --git a/tests/utilities.py b/tests/utilities.py new file mode 100644 index 00000000..aae79552 --- /dev/null +++ b/tests/utilities.py @@ -0,0 +1,100 @@ +# encoding=utf-8 +# Copyright © 2017 Dylan Baker +# +# This program 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. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +"""Helpers for unittests themselves.""" + +from __future__ import absolute_import + +import functools +import unittest + + +def _tear_down_class_wrapper(original, cls): + """Ensure that doClassCleanups is called after tearDownClass.""" + try: + original() + finally: + cls.doClassCleanups() + + +def _set_up_class_wrapper(original, cls): + """If setUpClass fails, call doClassCleanups.""" + try: + original() + except Exception: + cls.doClassCleanups() + raise + + +class TestCaseClassCleanup(unittest.TestCase): + + """A subclass of unittest.TestCase which adds classlevel clenups methods. + """ + + __stack = [] + + def __new__(cls, _): + """Wrap the tearDownClass method to esnure that doClassCleanups gets + called. + + Because doCleanups (the test instance level version of this + functionality) is called in code we can't supclass we need to do some + hacking to ensure it's called. that hackery is in the form of wrapping + the call to tearDownClass and setupClass methods. + """ + original = cls.tearDownClass + + # Get a unique object, otherwise functools.update_wrapper will always + # act on the same object. We're using functools.partial as a proxy to + # receive that information. + # + # We're also passing the original implementation to the wrapper + # function as an argument, because it is being passed an unbound class + # method the calling function will need to pass cls to it as an + # argument. + unique = functools.partial(_tear_down_class_wrapper, original, cls) + + # the classmethod decorator hides the __module__ attribute, so don't + # try to set it. In python 3.x this is no longer true and the lat + # parameter of this call can be removed + functools.update_wrapper(unique, original, ['__name__', '__doc__']) + + # Repalce the orinal tearDownClass method with our wrapper + cls.tearDownClass = unique + + # Do essentially the same thing for setup, but to ensure that + # doClassCleanups is only called if there is an exception in the + # setUpClass method. + original = cls.setUpClass + unique = functools.partial(_set_up_class_wrapper, original, cls) + functools.update_wrapper(unique, original, ['__name__', '__doc__']) + cls.setUpClass = unique + + return unittest.TestCase.__new__(cls) + + @classmethod + def addClassCleanup(cls, function, *args, **kwargs): # pylint: disable=invalid-name + cls.__stack.append((function, args, kwargs)) + + @classmethod + def doClassCleanups(cls): # pylint: disable=invalid-name + while cls.__stack: + func, args, kwargs = cls.__stack.pop() + + # TODO: Should exceptions be ignored from this? + # TODO: addCleanups success if part of the success of the test, + # what should we do here? + func(*args, **kwargs) |