diff --git a/Makefile b/Makefile index 7458256..77dd310 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ cleantest: clean rm gnupg/tests/*.log test: cleantest - python gnupg/tests/test_gnupg.py basic genkey sign + python gnupg/tests/test_gnupg.py parsers basic genkey sign install: python setup.py install --record installed-files.txt diff --git a/gnupg/gnupg.py b/gnupg/gnupg.py index 4bb417e..62b5747 100644 --- a/gnupg/gnupg.py +++ b/gnupg/gnupg.py @@ -137,26 +137,25 @@ def _copy_data(instream, outstream): if len(data) == 0: break sent += len(data) - logger.debug("sending chunk (%d): %r", sent, data[:256]) + logger.debug("_copy_data(): sending chunk (%d):\n%s" % (sent, data[:256])) try: outstream.write(data) except UnicodeError: try: outstream.write(data.encode(enc)) except IOError: - logger.exception('Error sending data: Broken pipe') + logger.exception('_copy_data(): Error sending data: Broken pipe') break except IOError: - # Can sometimes get 'broken pipe' errors even when the - # data has all been sent - logger.exception('Error sending data: Broken pipe') + # Can get 'broken pipe' errors even when all data was sent + logger.exception('_copy_data(): Error sending data: Broken pipe') break try: outstream.close() except IOError: - logger.exception('Got IOError while trying to close FD outstream') + logger.exception('_copy_data(): Got IOError while closing %s' % outstream) else: - logger.debug("closed output, %d bytes sent", sent) + logger.debug("_copy_data(): Closed output, %d bytes sent." % sent) def _threaded_copy_data(instream, outstream): """Copy data from one stream to another in a separate thread. @@ -413,10 +412,6 @@ class GPG(object): if isinstance(message, file): result = self._sign_file(message, **kwargs) elif not util._is_stream(message): - if isinstance(message, str): - if not util._py3k: - message = unicode(message, self.encoding) - message = message.encode(self.encoding) f = util._make_binary_stream(message, self.encoding) result = self._sign_file(f, **kwargs) f.close() @@ -429,7 +424,7 @@ class GPG(object): def _sign_file(self, file, keyid=None, passphrase=None, clearsign=True, detach=False, binary=False): """Create a signature for a file.""" - logger.debug("GPG._sign_file(): %s", file) + logger.debug("_sign_file(): %s", file) if binary: args = ['--sign'] else: @@ -438,10 +433,8 @@ class GPG(object): if clearsign: args.append("--clearsign") if detach: - logger.warn( - "Cannot use --clearsign and --detach-sign simultaneously.") - logger.warn( - "Using default GPG behaviour: --clearsign only.") + logger.warn("Cannot use both --clearsign and --detach-sign.") + logger.warn("Using default GPG behaviour: --clearsign only.") elif detach and not clearsign: args.append("--detach-sign") @@ -449,8 +442,8 @@ class GPG(object): args.append(str("--default-key %s" % keyid)) result = self._result_map['sign'](self) - #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. + ## 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. p = self._open_subprocess(args, passphrase is not None) try: stdin = p.stdin @@ -458,7 +451,7 @@ class GPG(object): _write_passphrase(stdin, passphrase, self.encoding) writer = _threaded_copy_data(file, stdin) except IOError: - logging.exception("error writing message") + logger.exception("_sign_file(): Error writing message") writer = None self._collect_output(p, result, writer, stdin) return result @@ -483,50 +476,43 @@ class GPG(object): f.close() return result - def verify_file(self, file, data_filename=None): - """ - Verify the signature on the contents of a file or file-like + def verify_file(self, file, sig_file=None): + """Verify the signature on the contents of a file or file-like object. Can handle embedded signatures as well as detached signatures. If using detached signatures, the file containing the - detached signature should be specified as the :param:data_filename. + detached signature should be specified as the ``sig_file``. :param file file: A file descriptor object. Its type will be checked - with :func:util._is_file. - :param file data_filename: A file containing the GPG signature data for - :param:file. If given, :param:file is - verified via this detached signature. + with :func:`util._is_file`. + :param str sig_file: A file containing the GPG signature data for + ``file``. If given, ``file`` is verified via this + detached signature. """ - ## attempt to wrap any escape characters in quotes: - safe_file = _fix_unsafe(file) - ## check that :param:file is actually a file: - util._is_file(safe_file) - - logger.debug('verify_file: %r, %r', safe_file, data_filename) + fn = None result = self._result_map['verify'](self) - args = ['--verify'] - if data_filename is None: - self._handle_io(args, safe_file, result, binary=True) + + if sig_file is None: + logger.debug("verify_file(): Handling embedded signature") + args = ["--verify"] + proc = self._open_subprocess(args) + writer = _threaded_copy_data(file, proc.stdin) + self._collect_output(proc, result, writer, stdin=proc.stdin) else: - safe_data_filename = _fix_unsafe(data_filename) - - logger.debug('Handling detached verification') - fd, fn = tempfile.mkstemp(prefix='pygpg') - - with open(safe_file) as sf: - contents = sf.read() - os.write(fd, s) - os.close(fd) - logger.debug('Wrote to temp file: %r', contents) - args.append(fn) - args.append('"%s"' % safe_data_filename) - - try: - p = self._open_subprocess(args) - self._collect_output(p, result, stdin=p.stdin) - finally: - os.unlink(fn) - + if not util._is_file(sig_file): + logger.debug("verify_file(): '%r' is not a file" % sig_file) + return result + logger.debug('verify_file(): Handling detached verification') + sig_fh = None + try: + sig_fh = open(sig_file) + args = ["--verify %s - " % sig_fh.name] + proc = self._open_subprocess(args) + writer = _threaded_copy_data(file, proc.stdin) + self._collect_output(proc, result, stdin=proc.stdin) + finally: + if sig_fh and not sig_fh.closed: + sig_fh.close() return result # @@ -659,13 +645,12 @@ class GPG(object): >>> pubkeys = gpg.list_keys() >>> assert print1 in pubkeys.fingerprints >>> assert print2 in pubkeys.fingerprints - """ which='public-keys' if secret: which='secret-keys' - args = "--list-%s --fixed-list-mode --fingerprint --with-colons" % (which,) + args = "--list-%s --fixed-list-mode --fingerprint --with-colons --no-show-photos" % (which,) args = [args] p = self._open_subprocess(args) @@ -692,6 +677,17 @@ class GPG(object): getattr(result, keyword)(L) return result + def list_sigs(self, *keyids): + """xxx implement me + + The GnuPG option '--show-photos', according to the GnuPG manual, "does + not work with --with-colons", but since we can't rely on all versions + of GnuPG to explicitly handle this correctly, we should probably + include it in the args. + """ + ## we will want to include "--no-show-photos" in the args + raise NotImplemented("Functionality for '--list-sigs' not implemented.") + def gen_key(self, input): """ Generate a key; you might use gen_key_input() to create the control @@ -871,8 +867,7 @@ class GPG(object): return result def decrypt(self, message, **kwargs): - """ - Decrypt the contents of a string or file-like object :param:message . + """Decrypt the contents of a string or file-like object ``message``. :param message: A string or file-like object to decrypt. """ @@ -967,9 +962,7 @@ class GPGWrapper(GPG): passphrase=passphrase) def send_keys(self, keyserver, *keyids): - """ - Send keys to a keyserver - """ + """Send keys to a keyserver.""" result = self._result_map['list'](self) gnupg.logger.debug('send_keys: %r', keyids) data = gnupg.util._make_binary_stream("", self.encoding) diff --git a/gnupg/parsers.py b/gnupg/parsers.py index 9bd8a68..8d434bb 100644 --- a/gnupg/parsers.py +++ b/gnupg/parsers.py @@ -34,6 +34,7 @@ import util ESCAPE_PATTERN = re.compile(r'\\x([0-9a-f][0-9a-f])', re.I) +HEXIDECIMAL = re.compile('([0-9A-F]{2})+') class ProtectedOption(Exception): @@ -268,16 +269,13 @@ def _is_allowed(input): ## assertion will check that GPG will recognise them ## ## xxx checkout the --store option for creating rfc1991 data packets - ## xxx also --multifile use with verify encrypt & decrypt ## xxx key fetching/retrieving options: [fetch_keys, merge_only, recv_keys] ## - ## xxx which ones do we want as defaults? - ## eg, --no-show-photos would mitigate things like - ## https://www-01.ibm.com/support/docview.wss?uid=swg21620982 _allowed = frozenset( ['--list-keys', '--list-key', '--fixed-list-mode', '--list-secret-keys', '--list-public-keys', '--list-packets', '--with-colons', + '--no-show-photos', '--delete-keys', '--delete-secret-keys', '--encrypt', '--encrypt-files', '--decrypt', '--decrypt-files', @@ -327,6 +325,17 @@ def _is_allowed(input): return input return None +def _is_hex(string): + """Check that a string is hexidecimal, with alphabetic characters + capitalized and without whitespace. + + :param str string: The string to check. + """ + matched = HEXIDECIMAL.match(string) + if matched is not None and len(matched.group()) >= 2: + return True + return False + def _sanitise(*args): """Take an arg or the key portion of a kwarg and check that it is in the set of allowed GPG options and flags, and that it has the correct @@ -388,65 +397,93 @@ def _sanitise(*args): if flag in ['--encrypt', '--encrypt-files', '--decrypt', '--decrypt-file', '--import', '--verify']: ## Place checks here: - if _is_file(val): + if util._is_file(val): safe_option += (val + " ") else: logger.debug("_check_option(): %s not file: %s" % (flag, val)) + elif flag in ['--default-key']: + if _is_hex(val): + safe_option += (val + " ") + else: + logger.debug("_check_option(): '%s %s' not hex." + % (flag, val)) else: safe_option += (val + " ") logger.debug("_check_option(): No checks for %s" % val) return safe_option - is_flag = lambda x: x.startswith('-') - checked = [] + is_flag = lambda x: x.startswith('--') + + def _make_filo(args_string): + filo = arg.split(' ') + filo.reverse() + logger.debug("_make_filo(): Converted to reverse list: %s" % filo) + return filo + + def _make_groups(filo): + groups = {} + while len(filo) >= 1: + last = filo.pop() + if is_flag(last): + logger.debug("_make_groups(): Got arg: %s" % last) + if last == '--verify': + groups[last] = str(filo.pop()) + ## accept the read-from-stdin arg: + if len(filo) >= 1 and filo[len(filo)-1] == '-': + groups[last] += str(' - \'\'') ## gross hack + else: + groups[last] = str() + while len(filo) > 1 and not is_flag(filo[len(filo)-1]): + logger.debug("_make_groups(): Got value: %s" + % filo[len(filo)-1]) + groups[last] += (filo.pop() + " ") + else: + if len(filo) == 1 and not is_flag(filo[0]): + logger.debug("_make_groups(): Got value: %s" % filo[0]) + groups[last] += filo.pop() + else: + logger.debug("_make_groups(): Got solitary value: %s" % last) + groups["xxx"] = last + return groups + + def _check_groups(groups): + logger.debug("_check_groups(): Got groups: %s" % groups) + checked_groups = [] + for a,v in groups.items(): + v = None if len(v) == 0 else v + safe = _check_option(a, v) + if safe is not None and not safe.strip() == "": + logger.debug("_check_groups(): appending option: %s" % safe) + checked_groups.append(safe) + else: + logger.debug("_check_groups(): dropped option '%s %s'" % (a,v)) + return checked_groups + if args is not None: + option_groups = {} for arg in args: + ## if we're given a string with a bunch of options in it split them + ## up and deal with them separately if isinstance(arg, str): logger.debug("_sanitise(): Got arg string: %s" % arg) - ## if we're given a string with a bunch of options in it split - ## them up and deal with them separately if arg.find(' ') > 0: - filo = arg.split() - filo.reverse() - new_arg, new_value = str(), str() - while len(filo) > 0: - if not is_flag(filo[0]): - logger.debug("_sanitise(): Got non-flag arg %s" - % filo[0]) - new_value += (filo.pop() + " ") - else: - logger.debug("_sanitise(): Got arg: %s" % filo[0]) - new_arg = filo.pop() - if len(filo) > 0: - while not is_flag(filo[0]): - logger.debug("_sanitise(): Got value: %s" - % filo[0]) - new_value += (filo.pop() + " ") - safe = _check_option(new_arg, new_value) - if safe is not None and not safe.strip() == "": - logger.debug("_sanitise(): appending option: %s" - % safe) - checked.append(safe) + filo = _make_filo(arg) + option_groups.update(_make_groups(filo)) else: - safe = _check_option(arg, None) - if safe is not None: - logger.debug("_sanitise(): appending args: %s" % safe) - checked.append(safe) - else: - logger.debug("_sanitise(): got None for safe") + option_groups.update({ arg: "" }) elif isinstance(arg, list): logger.debug("_sanitise(): Got arg list: %s" % arg) - allow = _one_flag(arg) - if allow is not None: - checked.append(allow) + arg.reverse() + option_groups.update(_make_groups(arg)) else: - logger.debug("_sanitise(): got non string or list arg: %s" - % arg) - - sanitised = ' '.join(x for x in checked) - return sanitised + logger.debug("_sanitise(): Got non str or list arg: %s" % arg) + checked = _check_groups(option_groups) + sanitised = ' '.join(x for x in checked) + return sanitised + else: + logger.debug("_sanitise(): Got None for args") def _sanitise_list(arg_list): """A generator for iterating through a list of gpg options and sanitising @@ -474,30 +511,48 @@ class Verify(object): TRUST_FULLY = 3 TRUST_ULTIMATE = 4 - TRUST_LEVELS = { - "TRUST_UNDEFINED" : TRUST_UNDEFINED, - "TRUST_NEVER" : TRUST_NEVER, - "TRUST_MARGINAL" : TRUST_MARGINAL, - "TRUST_FULLY" : TRUST_FULLY, - "TRUST_ULTIMATE" : TRUST_ULTIMATE, - } + TRUST_LEVELS = { "TRUST_UNDEFINED" : TRUST_UNDEFINED, + "TRUST_NEVER" : TRUST_NEVER, + "TRUST_MARGINAL" : TRUST_MARGINAL, + "TRUST_FULLY" : TRUST_FULLY, + "TRUST_ULTIMATE" : TRUST_ULTIMATE, } + + #: True if the signature is valid, False otherwise. + valid = False + #: A string describing the status of the signature verification. + #: Can be one of ``'signature bad'``, ``'signature good'``, + #: ``'signature valid'``, ``'signature error'``, ``'decryption failed'``, + #: ``'no public key'``, ``'key exp'``, or ``'key rev'``. + status = None + #: The fingerprint of the signing keyid. + fingerprint = None + #: The fingerprint of the corresponding public key, which may be different + #: if the signature was created with a subkey. + pubkey_fingerprint = None + #: The keyid of the signing key. + key_id = None + #: xxx I'm not sure how this is different to key_id. + signature_id = None + #: The creation date of the signing key. + creation_date = None + #: The timestamp of the purported signature, if we are unable to parse it. + timestamp = None + #: The userid of the signing key which was used to create the signature. + username = None + #: When the signing key is due to expire. + expire_timestamp = None + #: The timestamp for when the signature was created. + sig_timestamp = None + #: A number 0-4 describing the trust level of the signature. + trust_level = None + #: The string corresponding to the ``trust_level`` number. + trust_text = None def __init__(self, gpg): self.gpg = gpg - self.valid = False - self.fingerprint = self.creation_date = self.timestamp = None - self.signature_id = self.key_id = None - self.username = None - self.status = None - self.pubkey_fingerprint = None - self.expire_timestamp = None - self.sig_timestamp = None - self.trust_text = None - self.trust_level = None def __nonzero__(self): return self.valid - __bool__ = __nonzero__ def handle_status(self, key, value): @@ -521,7 +576,6 @@ class Verify(object): self.creation_date, self.sig_timestamp, self.expire_timestamp) = value.split()[:4] - # may be different if signature is made with a subkey self.pubkey_fingerprint = value.split()[-1] self.status = 'signature valid' elif key == "SIG_ID": @@ -574,7 +628,7 @@ class Crypt(Verify): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. @@ -676,19 +730,14 @@ class Sign(object): #: The type of signature created. sig_type = None - #: The algorithm used to create the signature. sig_algo = None - #: The hash algorithm used to create the signature. sig_hash_also = None - #: The fingerprint of the signing keyid. fingerprint = None - #: The timestamp on the signature. timestamp = None - #: xxx fill me in what = None @@ -705,7 +754,7 @@ class Sign(object): __bool__ = __nonzero__ def __str__(self): - return self.data.decode(self.gpg.encoding, self.gpg.decode_errors) + return self.data.decode(self.gpg.encoding, self.gpg._decode_errors) def handle_status(self, key, value): """Parse a status code from the attached GnuPG process. diff --git a/gnupg/tests/files/cypherpunk_manifesto b/gnupg/tests/files/cypherpunk_manifesto new file mode 100644 index 0000000..9e3b503 --- /dev/null +++ b/gnupg/tests/files/cypherpunk_manifesto @@ -0,0 +1,87 @@ +Privacy is necessary for an open society in the electronic age. Privacy is not +secrecy. A private matter is something one doesn't want the whole world to +know, but a secret matter is something one doesn't want anybody to +know. Privacy is the power to selectively reveal oneself to the world. + +If two parties have some sort of dealings, then each has a memory of their +interaction. Each party can speak about their own memory of this; how could +anyone prevent it? One could pass laws against it, but the freedom of speech, +even more than privacy, is fundamental to an open society; we seek not to +restrict any speech at all. If many parties speak together in the same forum, +each can speak to all the others and aggregate together knowledge about +individuals and other parties. The power of electronic communications has +enabled such group speech, and it will not go away merely because we might want +it to. + +Since we desire privacy, we must ensure that each party to a transaction have +knowledge only of that which is directly necessary for that transaction. Since +any information can be spoken of, we must ensure that we reveal as little as +possible. In most cases personal identity is not salient. When I purchase a +magazine at a store and hand cash to the clerk, there is no need to know who I +am. When I ask my electronic mail provider to send and receive messages, my +provider need not know to whom I am speaking or what I am saying or what others +are saying to me; my provider only need know how to get the message there and +how much I owe them in fees. When my identity is revealed by the underlying +mechanism of the transaction, I have no privacy. I cannot here selectively +reveal myself; I must always reveal myself. + +Therefore, privacy in an open society requires anonymous transaction +systems. Until now, cash has been the primary such system. An anonymous +transaction system is not a secret transaction system. An anonymous system +empowers individuals to reveal their identity when desired and only when +desired; this is the essence of privacy. + +Privacy in an open society also requires cryptography. If I say something, I +want it heard only by those for whom I intend it. If the content of my speech +is available to the world, I have no privacy. To encrypt is to indicate the +desire for privacy, and to encrypt with weak cryptography is to indicate not +too much desire for privacy. Furthermore, to reveal one's identity with +assurance when the default is anonymity requires the cryptographic signature. + +We cannot expect governments, corporations, or other large, faceless +organizations to grant us privacy out of their beneficence. It is to their +advantage to speak of us, and we should expect that they will speak. To try to +prevent their speech is to fight against the realities of +information. Information does not just want to be free, it longs to be +free. Information expands to fill the available storage space. Information is +Rumor's younger, stronger cousin; Information is fleeter of foot, has more +eyes, knows more, and understands less than Rumor. + +We must defend our own privacy if we expect to have any. We must come together +and create systems which allow anonymous transactions to take place. People +have been defending their own privacy for centuries with whispers, darkness, +envelopes, closed doors, secret handshakes, and couriers. The technologies of +the past did not allow for strong privacy, but electronic technologies do. + +We the Cypherpunks are dedicated to building anonymous systems. We are +defending our privacy with cryptography, with anonymous mail forwarding +systems, with digital signatures, and with electronic money. + +Cypherpunks write code. We know that someone has to write software to defend +privacy, and since we can't get privacy unless we all do, we're going to write +it. We publish our code so that our fellow Cypherpunks may practice and play +with it. Our code is free for all to use, worldwide. We don't much care if you +don't approve of the software we write. We know that software can't be +destroyed and that a widely dispersed system can't be shut down. + +Cypherpunks deplore regulations on cryptography, for encryption is +fundamentally a private act. The act of encryption, in fact, removes +information from the public realm. Even laws against cryptography reach only so +far as a nation's border and the arm of its violence. Cryptography will +ineluctably spread over the whole globe, and with it the anonymous transactions +systems that it makes possible. + +For privacy to be widespread it must be part of a social contract. People must +come and together deploy these systems for the common good. Privacy only +extends so far as the cooperation of one's fellows in society. We the +Cypherpunks seek your questions and your concerns and hope we may engage you so +that we do not deceive ourselves. We will not, however, be moved out of our +course because some may disagree with our goals. + +The Cypherpunks are actively engaged in making the networks safer for +privacy. Let us proceed together apace. + +Onward. + +Eric Hughes +9 March 1993 diff --git a/gnupg/tests/test_gnupg.py b/gnupg/tests/test_gnupg.py index baba110..949ebc7 100644 --- a/gnupg/tests/test_gnupg.py +++ b/gnupg/tests/test_gnupg.py @@ -17,6 +17,7 @@ import os import shutil import sys import tempfile +import time ## Use unittest2 if we're on Python2.6 or less: if sys.version_info.major == 2 and sys.version_info.minor <= 6: @@ -45,6 +46,8 @@ def _make_tempfile(*args, **kwargs): *args, **kwargs) logger = logging.getLogger('gnupg') +_here = os.path.join(os.path.join(util._repo, 'gnupg'), 'tests') +_files = os.path.join(_here, 'files') KEYS_TO_IMPORT = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1.4.9 (MingW32) @@ -146,6 +149,26 @@ class GPGTestCase(unittest.TestCase): self.pubring = os.path.join(self.homedir, 'pubring.gpg') self.secring = os.path.join(self.homedir, 'secring.gpg') + def test_parsers_fix_unsafe(self): + """Test that unsafe inputs are quoted out and then ignored.""" + shell_input = "\"&coproc /bin/sh\"" + fixed = parsers._fix_unsafe(shell_input) + print fixed + test_file = os.path.join(_files, 'cypherpunk_manifesto') + self.assertTrue(os.path.isfile(test_file)) + has_shell = self.gpg.verify_file(test_file, fixed) + self.assertFalse(has_shell.valid) + + def test_parsers_is_hex_valid(self): + """Test that valid hexidecimal passes the parsers._is_hex() check""" + valid_hex = '0A6A58A14B5946ABDE18E207A3ADB67A2CDB8B35' + self.assertTrue(parsers._is_hex(valid_hex)) + + def test_parsers_is_hex_invalid(self): + """Test that invalid hexidecimal fails the parsers._is_hex() check""" + invalid_hex = 'cipherpunks write code' + self.assertFalse(parsers._is_hex(invalid_hex)) + def test_gpghome_creation(self): """Test the environment by ensuring that setup worked.""" hd = self.homedir @@ -271,51 +294,39 @@ class GPGTestCase(unittest.TestCase): self.assertGreater(key_input.find('Francisco Ferrer'), 0) def test_rsa_key_generation(self): - """ - Test that RSA key generation succeeds. - """ - key = self.generate_key("Barbara Brown", "beta.com") + """Test that RSA key generation succeeds.""" + key = self.generate_key("Ralph Merkle", "xerox.com") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_unicode(self): - """ - Test that RSA key generation succeeds with unicode characters. - """ + """Test that RSA key generation succeeds with unicode characters.""" key = self.generate_key("Anaïs de Flavigny", "êtrerien.fr") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_rsa_key_generation_with_subkey(self): - """ - Test that RSA key generation succeeds with additional subkey. - """ - key = self.generate_key("Need Caffeine", "nowplea.se", + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("John Gilmore", "isapu.nk", subkey_type='RSA') self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation(self): - """ - Test that DSA key generation succeeds. - """ - key = self.generate_key("DSA Signonly", "test.com") + """Test that DSA key generation succeeds.""" + key = self.generate_key("Ross Anderson", "bearli.on") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_unicode(self): - """ - Test that DSA key generation succeeds with unicode characters. - """ + """Test that DSA key generation succeeds with unicode characters.""" key = self.generate_key("破壊合計する", "破壊合計する.日本") self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) def test_dsa_key_generation_with_subkey(self): - """ - Test that RSA key generation succeeds with additional subkey. - """ - key = self.generate_key("OMG Moar Coffee", "giveitto.me", + """Test that RSA key generation succeeds with additional subkey.""" + key = self.generate_key("Eli Biham", "bearli.on", subkey_type='ELG-E') self.assertIsNotNone(key.type) self.assertIsNotNone(key.fingerprint) @@ -505,49 +516,45 @@ class GPGTestCase(unittest.TestCase): def test_signature_algorithm(self): """Test that determining the signing algorithm works.""" - key = self.generate_key("Werner Koch", "gnupg.org") - message = "Damn, I really wish GnuPG had ECC support." + key = self.generate_key("Ron Rivest", "rsa.com") + message = "Someone should add GCM block cipher mode to PyCrypto." sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='wernerkoch') + passphrase='ronrivest') print "ALGORITHM:\n", sig.sig_algo self.assertIsNotNone(sig.sig_algo) def test_signature_string_bad_passphrase(self): """Test that signing and verification works.""" - key = self.generate_key("Ron Rivest", "rsa.com") - message = 'Hello, André!' + key = self.generate_key("Taher ElGamal", "cryto.me") + message = 'أصحاب المصالح لا يحبون الثوراتز' sig = self.gpg.sign(message, keyid=key.fingerprint, passphrase='foo') self.assertFalse(sig, "Bad passphrase should fail") def test_signature_string_alternate_encoding(self): - key = self.generate_key("Adi Shamir", "rsa.com") + key = self.generate_key("Nos Oignons", "nos-oignons.net") self.gpg.encoding = 'latin-1' - message = 'Hello, André!' + message = "Mêle-toi de tes oignons" sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='adishamir') + passphrase='nosoignons') self.assertTrue(sig) def test_signature_file(self): """Test that signing a message file works.""" key = self.generate_key("Leonard Adleman", "rsa.com") - message = "Someone should add GCM block cipher mode to PyCrypto." - message_fn = os.path.join(tempfile.gettempdir(), 'test_signature_file') - with open(message_fn, 'w+b') as msg: - msg.write(message) - - message_file = buffer(open(message_fn, "rb").read()) - mf = io.BytesIO(message_file) - - sig = self.gpg.sign(mf, keyid=key.fingerprint, - passphrase='leonardadleman') - self.assertTrue(sig, "Good passphrase should succeed") + message_file = os.path.join(_files, 'cypherpunk_manifesto') + with open(message_file) as msg: + sig = self.gpg.sign(msg, keyid=key.fingerprint, + passphrase='leonardadleman') + self.assertTrue(sig, "I thought I typed my password correctly...") def test_signature_string_verification(self): """Test verification of a signature from a message string.""" - key = self.generate_key("Andrew Able", "alpha.com") - message = 'Hello, André!' + key = self.generate_key("Bruce Schneier", "schneier.com") + message = '...the government uses the general fear of ' + message += '[hackers in popular culture] to push for more power' sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='andrewable') + passphrase='bruceschneier') + now = time.mktime(time.gmtime()) self.assertTrue(sig, "Good passphrase should succeed") verified = self.gpg.verify(sig.data) self.assertIsNotNone(verified.fingerprint) @@ -556,15 +563,18 @@ class GPGTestCase(unittest.TestCase): logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") - self.assertEqual(verified.trust_level, verified.TRUST_ULTIMATE) - self.assertEqual(verified.trust_text, 'TRUST_ULTIMATE') + self.assertEqual(verified.status, 'signature valid') + self.assertAlmostEqual(int(now), int(verified.timestamp), delta=1000) + self.assertEqual( + verified.username, + u'Bruce Schneier (python-gnupg tester) ') - def test_signature_file_verification(self): - """Test verfication of a signature on a message file.""" - key = self.generate_key("Taher ElGamal", "cryto.me") - message = 'أصحاب المصالح لا يحبون الثوراتز' + def test_signature_verification_clearsign(self): + """Test verfication of an embedded signature.""" + key = self.generate_key("Johan Borst", "rijnda.el") + message = "You're *still* using AES? Really?" sig = self.gpg.sign(message, keyid=key.fingerprint, - passphrase='taherelgamal') + passphrase='johanborst') self.assertTrue(sig, "Good passphrase should succeed") try: file = util._make_binary_stream(sig.data, self.gpg.encoding) @@ -576,22 +586,41 @@ class GPGTestCase(unittest.TestCase): logger.debug("ver: %r", verified.fingerprint) self.assertEqual(key.fingerprint, verified.fingerprint, "Fingerprints must match") - data_file = open('random_binary_data', 'rb') - sig = self.gpg._sign_file(data_file, keyid=key.fingerprint, - passphrase='andrewable', detach=True) - data_file.close() - self.assertTrue(sig, "File signing should succeed") - try: - file = gnupg._make_binary_stream(sig.data, self.gpg.encoding) - verified = self.gpg.verify_file(file, 'random_binary_data') - except UnicodeDecodeError: #happens in Python 2.6 - verified = self.gpg.verify_file(io.BytesIO(sig.data)) - if key.fingerprint != verified.fingerprint: - logger.debug("key: %r", key.fingerprint) - logger.debug("ver: %r", verified.fingerprint) - self.assertEqual(key.fingerprint, verified.fingerprint, - "Fingerprints must match") - logger.debug("test_signature_verification ends") + + def test_signature_verification_detached(self): + """Test that verification of a detached signature of a file works.""" + key = self.generate_key("Paulo S.L.M. Barreto", "anub.is") + with open(os.path.join(_files, 'cypherpunk_manifesto'), + 'rb') as manifesto: + sig = self.gpg.sign(manifesto, keyid=key.fingerprint, + passphrase='paulos.l.m.barreto', + detach=True, clearsign=False) + self.assertTrue(sig.data, "File signing should succeed") + sigfilename = os.path.join(_files, 'cypherpunk_manifesto.sig') + with open(sigfilename,'w') as sigfile: + sigfile.write(sig.data) + sigfile.seek(0) + + verified = self.gpg.verify_file(manifesto, sigfilename) + + if key.fingerprint != verified.fingerprint: + logger.debug("key: %r", key.fingerprint) + logger.debug("ver: %r", verified.fingerprint) + + self.assertEqual(key.fingerprint, verified.fingerprint, + "Fingerprints must match") + + def test_signature_verification_detached_binary(self): + """Test that detached signature verification in binary mode fails.""" + key = self.generate_key("Adi Shamir", "rsa.com") + with open(os.path.join(_files, 'cypherpunk_manifesto'), + 'rb') as manifesto: + sig = self.gpg.sign(manifesto, keyid=key.fingerprint, + passphrase='adishamir', + detach=True, binary=True, clearsign=False) + self.assertTrue(sig.data, "File signing should succeed") + with self.assertRaises(UnicodeDecodeError): + print "SIG=", sig def test_deletion(self): """Test that key deletion works.""" @@ -639,7 +668,10 @@ class GPGTestCase(unittest.TestCase): logger.debug("test_file_encryption_and_decryption ends") -suites = { 'basic': set(['test_gpghome_creation', +suites = { 'parsers': set(['test_parsers_fix_unsafe', + 'test_parsers_is_hex_valid', + 'test_parsers_is_hex_invalid',]), + 'basic': set(['test_gpghome_creation', 'test_gpg_binary', 'test_gpg_binary_not_abs', 'test_gpg_binary_version_str', @@ -659,7 +691,9 @@ suites = { 'basic': set(['test_gpghome_creation', 'test_key_generation_with_empty_value', 'test_key_generation_override_default_value', 'test_key_generation_with_colons']), - 'sign': set(['test_signature_file_verification', + 'sign': set(['test_signature_verification_clearsign', + 'test_signature_verification_detached', + 'test_signature_verification_detached_binary', 'test_signature_file', 'test_signature_string_bad_passphrase', 'test_signature_string_alternate_encoding', diff --git a/gnupg/util.py b/gnupg/util.py index d963aca..169d4fd 100644 --- a/gnupg/util.py +++ b/gnupg/util.py @@ -140,7 +140,7 @@ def _is_file(input): """ try: assert os.lstat(input).st_size > 0, "not a file: %s" % input - except (AssertionError, TypeError) as error: + except (AssertionError, TypeError, IOError, OSError) as error: logger.debug(error.message) return False else: