From e90ae547387d39aa96215f47344c7d7816a04c31 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 18 Mar 2015 05:47:40 +0000 Subject: [PATCH 1/3] Add two signature passphrase tests for reproducing Issue #82. --- gnupg/test/test_gnupg.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index e8be11a..f2ac931 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -626,6 +626,36 @@ class GPGTestCase(unittest.TestCase): passphrase='wrong horse battery staple') self.assertFalse(sig, "Bad passphrase should fail") + def test_signature_string_passphrase_empty_string(self): + """Test that a signing attempt with passphrase='' creates a valid + signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase='') + self.assertTrue(sig) + self.assertTrue(message in str(sig)) + + def test_signature_string_passphrase_None(self): + """Test that a signing attempt with passphrase=None fails creates a + valid signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=None) + self.assertTrue(sig) + self.assertTrue(message in str(sig)) + def test_signature_file(self): """Test that signing a message file works.""" key = self.generate_key("Leonard Adleman", "rsa.com") @@ -1339,6 +1369,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_verification_detached', 'test_signature_verification_detached_binary', 'test_signature_file', + 'test_signature_string_passphrase_empty_string', + 'test_signature_string_passphrase_None', 'test_signature_string_bad_passphrase', 'test_signature_string_verification', 'test_signature_string_algorithm_encoding']), From 9be01ec6df15ddaad86a695524a90f5f801f4d60 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 18 Mar 2015 19:31:33 +0000 Subject: [PATCH 2/3] Add two more signature passphrase tests with bytes literals for #82. --- gnupg/test/test_gnupg.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/gnupg/test/test_gnupg.py b/gnupg/test/test_gnupg.py index f2ac931..da99ca2 100755 --- a/gnupg/test/test_gnupg.py +++ b/gnupg/test/test_gnupg.py @@ -641,6 +641,36 @@ class GPGTestCase(unittest.TestCase): self.assertTrue(sig) self.assertTrue(message in str(sig)) + def test_signature_string_passphrase_empty_bytes_literal(self): + """Test that a signing attempt with passphrase=b'' creates a valid + signature. + + See Issue #82: https://github.com/isislovecruft/python-gnupg/issues/82 + """ + with open(os.path.join(_files, 'test_key_1.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=b'') + self.assertTrue(sig) + print("%r" % str(sig)) + self.assertTrue(message in str(sig)) + + def test_signature_string_passphrase_bytes_literal(self): + """Test that a signing attempt with passphrase=b'overalls' creates a + valid signature. + """ + with open(os.path.join(_files, 'kat.sec')) as fh1: + res1 = self.gpg.import_keys(fh1.read()) + key1 = res1.fingerprints[0] + + message = 'abc\ndef\n' + sig = self.gpg.sign(message, default_key=key1, passphrase=b'overalls') + self.assertTrue(sig) + print("%r" % str(sig)) + self.assertTrue(message in str(sig)) + def test_signature_string_passphrase_None(self): """Test that a signing attempt with passphrase=None fails creates a valid signature. @@ -1370,6 +1400,8 @@ suites = { 'parsers': set(['test_parsers_fix_unsafe', 'test_signature_verification_detached_binary', 'test_signature_file', 'test_signature_string_passphrase_empty_string', + 'test_signature_string_passphrase_empty_bytes_literal', + 'test_signature_string_passphrase_bytes_literal', 'test_signature_string_passphrase_None', 'test_signature_string_bad_passphrase', 'test_signature_string_verification', From 16107bc8a8be14bb8fedbf2a2eeea879d5f737c5 Mon Sep 17 00:00:00 2001 From: Isis Lovecruft Date: Wed, 18 Mar 2015 19:33:09 +0000 Subject: [PATCH 3/3] Workaround possible upstream GnuPG bug sign fail when passphrase=''. If passed `--passphrase-fd 0` and piped the passphrase `''` (an empty string), GnuPG will truncate the message being signed. The truncation appears to only affect messages which contain newlines; for those messages, the first line (up unto and including the newline character) will be truncated and thus missing from the output signed message or signature, causing verification for signatures on the original non-truncated message to fail. * FIXES #82: https://github.com/isislovecruft/python-gnupg/issues/82 --- gnupg/_meta.py | 15 +++++++++++++++ gnupg/_util.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/gnupg/_meta.py b/gnupg/_meta.py index ed0babf..20d5426 100644 --- a/gnupg/_meta.py +++ b/gnupg/_meta.py @@ -46,6 +46,8 @@ except ImportError: from . import _parsers from . import _util +from ._util import b +from ._util import s from ._parsers import _check_preferences from ._parsers import _sanitise_list @@ -804,6 +806,19 @@ class GPGBase(object): ## We could use _handle_io here except for the fact that if the ## passphrase is bad, gpg bails and you can't write the message. result = self._result_map['sign'](self) + + ## If the passphrase is an empty string, the message up to and + ## including its first newline will be cut off before making it to the + ## GnuPG process. Therefore, if the passphrase='' or passphrase=b'', + ## we set passphrase=None. See Issue #82: + ## https://github.com/isislovecruft/python-gnupg/issues/82 + if _util._is_string(passphrase): + passphrase = passphrase if len(passphrase) > 0 else None + elif _util._is_bytes(passphrase): + passphrase = s(passphrase) if len(passphrase) > 0 else None + else: + passphrase = None + proc = self._open_subprocess(args, passphrase is not None) try: if passphrase: diff --git a/gnupg/_util.py b/gnupg/_util.py index ba37a7a..d25aa92 100644 --- a/gnupg/_util.py +++ b/gnupg/_util.py @@ -165,6 +165,33 @@ def find_encodings(enc=None, system=False): return coder + +if _py3k: + def b(x): + """See http://python3porting.com/problems.html#nicer-solutions""" + return x + + def s(x): + if isinstance(x, str): + return x + elif isinstance(x, (bytes, bytearray)): + return x.decode(find_encodings().name) + else: + raise NotImplemented +else: + def b(x): + """See http://python3porting.com/problems.html#nicer-solutions""" + return find_encodings().encode(x)[0] + + def s(x): + if isinstance(x, basestring): + return x + elif isinstance(x, (bytes, bytearray)): + return x.decode(find_encodings().name) + else: + raise NotImplemented + + def author_info(name, contact=None, public_key=None): """Easy object-oriented representation of contributor info. @@ -440,6 +467,31 @@ def _is_stream(input): """ return isinstance(input, tuple(_STREAMLIKE_TYPES)) +def _is_string(thing): + """Check that **thing** is a string. The definition of the latter depends + upon the Python version. + + :param thing: The thing to check if it's a string. + :rtype: bool + :returns: ``True`` if **thing** is string (or unicode in Python2). + """ + if (_py3k and isinstance(thing, str)): + return True + if (not _py3k and isinstance(thing, basestring)): + return True + return False + +def _is_bytes(thing): + """Check that **thing** is bytes. + + :param thing: The thing to check if it's bytes. + :rtype: bool + :returns: ``True`` if **thing** is bytes or a bytearray. + """ + if isinstance(thing, (bytes, bytearray)): + return True + return False + def _is_list_or_tuple(instance): """Check that ``instance`` is a list or tuple.