python: Add an idiomatic interface.
authorJustus Winter <justus@g10code.com>
Wed, 8 Jun 2016 16:58:57 +0000 (18:58 +0200)
committerJustus Winter <justus@g10code.com>
Fri, 15 Jul 2016 16:28:09 +0000 (18:28 +0200)
* configure.ac: Bump required Python version.
* lang/python/pyme/__init__.py: Update docstring.  Import Context and
Data.
* lang/python/pyme/core.py (Context.encrypt): New function.
(Context.decrypt): Likewise.
(Context.sign): Likewise.
(Context.verify): Likewise.
* lang/python/pyme/errors.py: Add new errors.
* lang/python/pyme/util.py (process_constants): Rework and return the
inserted keys.
* lang/python/tests/Makefile.am (EXTRA_DIST): Add new keys.
* lang/python/tests/encrypt-only.asc: New file.
* lang/python/tests/sign-only.asc: Likewise.
* lang/python/tests/initial.py: Mark key 'Alpha' as trusted, import
new keys.
* lang/python/tests/support.py: Add fingerprints of known keys.
(in_srcdir): New function.
(print_data): Handle bytes too.
(mark_key_trusted): New function.
* lang/python/tests/t-decrypt-verify.py: Adjust test.  Test idiomatic
interface.
* lang/python/tests/t-decrypt.py: Test idiomatic interface.
* lang/python/tests/t-encrypt-sign.py: Likewise.
* lang/python/tests/t-encrypt-sym.py: Likewise.
* lang/python/tests/t-encrypt.py: Likewise.
* lang/python/tests/t-idiomatic.py: Simplify.
* lang/python/tests/t-keylist.py: Adjust to newly trusted key.
* lang/python/tests/t-sign.py: Likewise.  Test idiomatic interface.
* lang/python/tests/t-signers.py: Likewise.
* lang/python/tests/t-verify.py: Likewise.

Signed-off-by: Justus Winter <justus@g10code.com>
20 files changed:
configure.ac
lang/python/pyme/__init__.py
lang/python/pyme/core.py
lang/python/pyme/errors.py
lang/python/pyme/util.py
lang/python/tests/Makefile.am
lang/python/tests/encrypt-only.asc [new file with mode: 0644]
lang/python/tests/initial.py
lang/python/tests/sign-only.asc [new file with mode: 0644]
lang/python/tests/support.py
lang/python/tests/t-decrypt-verify.py
lang/python/tests/t-decrypt.py
lang/python/tests/t-encrypt-sign.py
lang/python/tests/t-encrypt-sym.py
lang/python/tests/t-encrypt.py
lang/python/tests/t-idiomatic.py
lang/python/tests/t-keylist.py
lang/python/tests/t-sign.py
lang/python/tests/t-signers.py
lang/python/tests/t-verify.py

index d395e00..6a7df24 100644 (file)
@@ -363,7 +363,7 @@ if test "$found" = "1"; then
             enabled_languages=$(echo $enabled_languages | sed 's/python//')
         fi
     else
-        AM_PATH_PYTHON([3.3])
+        AM_PATH_PYTHON([3.4])
         AX_SWIG_PYTHON
        if test -z "$PYTHON_VERSION"; then
            if test "$explicit_languages" = "1"; then
index e377f59..c42f794 100644 (file)
@@ -40,6 +40,20 @@ FEATURES
 
  * Fully object-oriented with convenient classes and modules.
 
+QUICK EXAMPLE
+-------------
+
+    >>> import pyme
+    >>> with pyme.Context() as c:
+    >>> with pyme.Context() as c:
+    ...     cipher, _, _ = c.encrypt("Hello world :)".encode(),
+    ...                              passphrase="abc")
+    ...     c.decrypt(cipher, passphrase="abc")
+    ...
+    (b'Hello world :)',
+     <pyme.results.DecryptResult object at 0x7f5ab8121080>,
+     <pyme.results.VerifyResult object at 0x7f5ab81219b0>)
+
 GENERAL OVERVIEW
 ----------------
 
@@ -78,59 +92,14 @@ do not appear explicitly anywhere. You can use dir() python built-in command
 on an object to see what methods and fields it has but their meaning can
 be found only in GPGME documentation.
 
-QUICK START SAMPLE PROGRAM
---------------------------
-This program is not for serious encryption, but for example purposes only!
-
-import sys
-import os
-from pyme import core, constants
-
-# Set up our input and output buffers.
-
-plain = core.Data('This is my message.')
-cipher = core.Data()
-
-# Initialize our context.
-
-c = core.Context()
-c.set_armor(1)
-
-# Set up the recipients.
-
-sys.stdout.write("Enter name of your recipient: ")
-sys.stdout.flush()
-name = sys.stdin.readline().strip()
-c.op_keylist_start(name, 0)
-r = c.op_keylist_next()
-
-# Do the encryption.
-
-c.op_encrypt([r], 1, plain, cipher)
-cipher.seek(0, os.SEEK_SET)
-sys.stdout.buffer.write(cipher.read())
-
-Note that although there is no explicit error checking done here, the
-Python GPGME library is automatically doing error-checking, and will
-raise an exception if there is any problem.
-
-This program is in the Pyme distribution as examples/simple.py.  The examples
-directory contains more advanced samples as well.
-
 FOR MORE INFORMATION
 --------------------
-PYME homepage: http://pyme.sourceforge.net
-GPGME documentation: http://pyme.sourceforge.net/doc/gpgme/index.html
-GPGME homepage: http://www.gnupg.org/gpgme.html
-
-Base classes: pyme.core (START HERE!)
-Error classes: pyme.errors
-Constants: pyme.constants
-Version information: pyme.version
-Utilities: pyme.util
-
-Base classes are documented at pyme.core.
+PYME3 homepage: https://www.gnupg.org/
+GPGME documentation: https://www.gnupg.org/documentation/manuals/gpgme/
 
 """
 
 __all__ = ['core', 'errors', 'constants', 'util', 'callbacks', 'version']
+
+from .core import Context
+from .core import Data
index e5ccf7c..6ca8cb8 100644 (file)
@@ -25,6 +25,7 @@ and the 'Data' class describing buffers of data.
 """
 
 import re
+import os
 import weakref
 from . import pygpgme
 from .errors import errorcheck, GPGMEError
@@ -166,6 +167,303 @@ class Context(GpgmeWrapper):
 
     """
 
+    def __init__(self, armor=False, textmode=False, offline=False,
+                 signers=[], pinentry_mode=constants.PINENTRY_MODE_DEFAULT,
+                 wrapped=None):
+        """Construct a context object
+
+        Keyword arguments:
+        armor          -- enable ASCII armoring (default False)
+        textmode       -- enable canonical text mode (default False)
+        offline                -- do not contact external key sources (default False)
+        signers                -- list of keys used for signing (default [])
+        pinentry_mode  -- pinentry mode (default PINENTRY_MODE_DEFAULT)
+
+        """
+        if wrapped:
+            self.own = False
+        else:
+            tmp = pygpgme.new_gpgme_ctx_t_p()
+            errorcheck(pygpgme.gpgme_new(tmp))
+            wrapped = pygpgme.gpgme_ctx_t_p_value(tmp)
+            pygpgme.delete_gpgme_ctx_t_p(tmp)
+            self.own = True
+        super().__init__(wrapped)
+        self.armor = armor
+        self.textmode = textmode
+        self.offline = offline
+        self.signers = signers
+        self.pinentry_mode = pinentry_mode
+
+    def encrypt(self, plaintext, recipients=[], sign=True, sink=None,
+                passphrase=None, always_trust=False, add_encrypt_to=False,
+                prepare=False, expect_sign=False, compress=True):
+        """Encrypt data
+
+        Encrypt the given plaintext for the given recipients.  If the
+        list of recipients is empty, the data is encrypted
+        symmetrically with a passphrase.
+
+        The passphrase can be given as parameter, using a callback
+        registered at the context, or out-of-band via pinentry.
+
+        Keyword arguments:
+        recipients     -- list of keys to encrypt to
+        sign           -- sign plaintext (default True)
+        sink           -- write result to sink instead of returning it
+        passphrase     -- for symmetric encryption
+        always_trust   -- always trust the keys (default False)
+        add_encrypt_to -- encrypt to configured additional keys (default False)
+        prepare                -- (ui) prepare for encryption (default False)
+        expect_sign    -- (ui) prepare for signing (default False)
+        compress       -- compress plaintext (default True)
+
+        Returns:
+        ciphertext     -- the encrypted data (or None if sink is given)
+        result         -- additional information about the encryption
+        sign_result    -- additional information about the signature(s)
+
+        Raises:
+        InvalidRecipients -- if encryption using a particular key failed
+        InvalidSigners -- if signing using a particular key failed
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        ciphertext = sink if sink else Data()
+        flags = 0
+        flags |= always_trust * constants.ENCRYPT_ALWAYS_TRUST
+        flags |= (not add_encrypt_to) * constants.ENCRYPT_NO_ENCRYPT_TO
+        flags |= prepare * constants.ENCRYPT_PREPARE
+        flags |= expect_sign * constants.ENCRYPT_EXPECT_SIGN
+        flags |= (not compress) * constants.ENCRYPT_NO_COMPRESS
+
+        if passphrase != None:
+            old_pinentry_mode = self.pinentry_mode
+            old_passphrase_cb = getattr(self, '_passphrase_cb', None)
+            self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK
+            def passphrase_cb(hint, desc, prev_bad, hook=None):
+                return passphrase
+            self.set_passphrase_cb(passphrase_cb)
+
+        try:
+            if sign:
+                self.op_encrypt_sign(recipients, flags, plaintext, ciphertext)
+            else:
+                self.op_encrypt(recipients, flags, plaintext, ciphertext)
+        except errors.GPGMEError as e:
+            if e.getcode() == errors.UNUSABLE_PUBKEY:
+                result = self.op_encrypt_result()
+                if result.invalid_recipients:
+                    raise errors.InvalidRecipients(result.invalid_recipients)
+            if e.getcode() == errors.UNUSABLE_SECKEY:
+                sig_result = self.op_sign_result()
+                if sig_result.invalid_signers:
+                    raise errors.InvalidSigners(sig_result.invalid_signers)
+            raise
+        finally:
+            if passphrase != None:
+                self.pinentry_mode = old_pinentry_mode
+                if old_passphrase_cb:
+                    self.set_passphrase_cb(*old_passphrase_cb[1:])
+
+        result = self.op_encrypt_result()
+        assert not result.invalid_recipients
+        sig_result = self.op_sign_result() if sign else None
+        assert not sig_result or not sig_result.invalid_signers
+
+        cipherbytes = None
+        if not sink:
+            ciphertext.seek(0, os.SEEK_SET)
+            cipherbytes = ciphertext.read()
+        return cipherbytes, result, sig_result
+
+    def decrypt(self, ciphertext, sink=None, passphrase=None, verify=True):
+        """Decrypt data
+
+        Decrypt the given ciphertext and verify any signatures.  If
+        VERIFY is an iterable of keys, the ciphertext must be signed
+        by all those keys, otherwise an error is raised.
+
+        If the ciphertext is symmetrically encrypted using a
+        passphrase, that passphrase can be given as parameter, using a
+        callback registered at the context, or out-of-band via
+        pinentry.
+
+        Keyword arguments:
+        sink           -- write result to sink instead of returning it
+        passphrase     -- for symmetric decryption
+        verify         -- check signatures (default True)
+
+        Returns:
+        plaintext      -- the decrypted data (or None if sink is given)
+        result         -- additional information about the decryption
+        verify_result  -- additional information about the signature(s)
+
+        Raises:
+        UnsupportedAlgorithm -- if an unsupported algorithm was used
+        BadSignatures  -- if a bad signature is encountered
+        MissingSignatures -- if expected signatures are missing or bad
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        plaintext = sink if sink else Data()
+
+        if passphrase != None:
+            old_pinentry_mode = self.pinentry_mode
+            old_passphrase_cb = getattr(self, '_passphrase_cb', None)
+            self.pinentry_mode = constants.PINENTRY_MODE_LOOPBACK
+            def passphrase_cb(hint, desc, prev_bad, hook=None):
+                return passphrase
+            self.set_passphrase_cb(passphrase_cb)
+
+        try:
+            if verify:
+                self.op_decrypt_verify(ciphertext, plaintext)
+            else:
+                self.op_decrypt(ciphertext, plaintext)
+        finally:
+            if passphrase != None:
+                self.pinentry_mode = old_pinentry_mode
+                if old_passphrase_cb:
+                    self.set_passphrase_cb(*old_passphrase_cb[1:])
+
+        result = self.op_decrypt_result()
+        verify_result = self.op_verify_result() if verify else None
+        if result.unsupported_algorithm:
+            raise errors.UnsupportedAlgorithm(result.unsupported_algorithm)
+
+        if verify:
+            if any(s.status != errors.NO_ERROR
+                   for s in verify_result.signatures):
+                raise errors.BadSignatures(verify_result)
+
+        if verify and verify != True:
+            missing = list()
+            for key in verify:
+                ok = False
+                for subkey in key.subkeys:
+                    for sig in verify_result.signatures:
+                        if sig.summary & constants.SIGSUM_VALID == 0:
+                            continue
+                        if subkey.can_sign and subkey.fpr == sig.fpr:
+                            ok = True
+                            break
+                    if ok:
+                        break
+                if not ok:
+                    missing.append(key)
+            if missing:
+                raise errors.MissingSignatures(verify_result, missing)
+
+        plainbytes = None
+        if not sink:
+            plaintext.seek(0, os.SEEK_SET)
+            plainbytes = plaintext.read()
+        return plainbytes, result, verify_result
+
+    def sign(self, data, sink=None, mode=constants.SIG_MODE_NORMAL):
+        """Sign data
+
+        Sign the given data with either the configured default local
+        key, or the 'signers' keys of this context.
+
+        Keyword arguments:
+        mode           -- signature mode (default: normal, see below)
+        sink           -- write result to sink instead of returning it
+
+        Returns:
+        either
+          signed_data  -- encoded data and signature (normal mode)
+          signature    -- only the signature data (detached mode)
+          cleartext    -- data and signature as text (cleartext mode)
+            (or None if sink is given)
+        result         -- additional information about the signature(s)
+
+        Raises:
+        InvalidSigners -- if signing using a particular key failed
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        signeddata = sink if sink else Data()
+
+        try:
+            self.op_sign(data, signeddata, mode)
+        except errors.GPGMEError as e:
+            if e.getcode() == errors.UNUSABLE_SECKEY:
+                result = self.op_sign_result()
+                if result.invalid_signers:
+                    raise errors.InvalidSigners(result.invalid_signers)
+            raise
+
+        result = self.op_sign_result()
+        assert not result.invalid_signers
+
+        signedbytes = None
+        if not sink:
+            signeddata.seek(0, os.SEEK_SET)
+            signedbytes = signeddata.read()
+        return signedbytes, result
+
+    def verify(self, signed_data, signature=None, sink=None, verify=[]):
+        """Verify signatures
+
+        Verify signatures over data.  If VERIFY is an iterable of
+        keys, the ciphertext must be signed by all those keys,
+        otherwise an error is raised.
+
+        Keyword arguments:
+        signature      -- detached signature data
+        sink           -- write result to sink instead of returning it
+
+        Returns:
+        data           -- the plain data
+            (or None if sink is given, or we verified a detached signature)
+        result         -- additional information about the signature(s)
+
+        Raises:
+        BadSignatures  -- if a bad signature is encountered
+        MissingSignatures -- if expected signatures are missing or bad
+        GPGMEError     -- as signaled by the underlying library
+
+        """
+        if signature:
+            # Detached signature, we don't return the plain text.
+            data = None
+        else:
+            data = sink if sink else Data()
+
+        if signature:
+            self.op_verify(signature, signed_data, None)
+        else:
+            self.op_verify(signed_data, None, data)
+
+        result = self.op_verify_result()
+        if any(s.status != errors.NO_ERROR for s in result.signatures):
+            raise errors.BadSignatures(result)
+
+        missing = list()
+        for key in verify:
+            ok = False
+            for subkey in key.subkeys:
+                for sig in result.signatures:
+                    if sig.summary & constants.SIGSUM_VALID == 0:
+                        continue
+                    if subkey.can_sign and subkey.fpr == sig.fpr:
+                        ok = True
+                        break
+                if ok:
+                    break
+            if not ok:
+                missing.append(key)
+        if missing:
+            raise errors.MissingSignatures(result, missing)
+
+        plainbytes = None
+        if data and not sink:
+            data.seek(0, os.SEEK_SET)
+            plainbytes = data.read()
+        return plainbytes, result
+
     @property
     def signers(self):
         """Keys used for signing"""
@@ -204,32 +502,6 @@ class Context(GpgmeWrapper):
         return 0
 
     _boolean_properties = {'armor', 'textmode', 'offline'}
-    def __init__(self, armor=False, textmode=False, offline=False,
-                 signers=[], pinentry_mode=constants.PINENTRY_MODE_DEFAULT,
-                 wrapped=None):
-        """Construct a context object
-
-        Keyword arguments:
-        armor          -- enable ASCII armoring (default False)
-        textmode       -- enable canonical text mode (default False)
-        offline                -- do not contact external key sources (default False)
-        signers                -- list of keys used for signing (default [])
-        pinentry_mode  -- pinentry mode (default PINENTRY_MODE_DEFAULT)
-        """
-        if wrapped:
-            self.own = False
-        else:
-            tmp = pygpgme.new_gpgme_ctx_t_p()
-            errorcheck(pygpgme.gpgme_new(tmp))
-            wrapped = pygpgme.gpgme_ctx_t_p_value(tmp)
-            pygpgme.delete_gpgme_ctx_t_p(tmp)
-            self.own = True
-        super().__init__(wrapped)
-        self.armor = armor
-        self.textmode = textmode
-        self.offline = offline
-        self.signers = signers
-        self.pinentry_mode = pinentry_mode
 
     def __del__(self):
         if not pygpgme:
index f96877b..0194931 100644 (file)
@@ -20,7 +20,10 @@ from . import util
 
 util.process_constants('GPG_ERR_', globals())
 
-class GPGMEError(Exception):
+class PymeError(Exception):
+    pass
+
+class GPGMEError(PymeError):
     def __init__(self, error = None, message = None):
         self.error = error
         self.message = message
@@ -43,8 +46,60 @@ class GPGMEError(Exception):
         return pygpgme.gpgme_err_source(self.error)
 
     def __str__(self):
-        return "%s (%d,%d)"%(self.getstring(), self.getsource(), self.getcode())
+        return self.getstring()
 
 def errorcheck(retval, extradata = None):
     if retval:
         raise GPGMEError(retval, extradata)
+
+# These errors are raised in the idiomatic interface code.
+
+class EncryptionError(PymeError):
+    pass
+
+class InvalidRecipients(EncryptionError):
+    def __init__(self, recipients):
+        self.recipients = recipients
+    def __str__(self):
+        return ", ".join("{}: {}".format(r.fpr,
+                                         pygpgme.gpgme_strerror(r.reason))
+                         for r in self.recipients)
+
+class DeryptionError(PymeError):
+    pass
+
+class UnsupportedAlgorithm(DeryptionError):
+    def __init__(self, algorithm):
+        self.algorithm = algorithm
+    def __str__(self):
+        return self.algorithm
+
+class SigningError(PymeError):
+    pass
+
+class InvalidSigners(SigningError):
+    def __init__(self, signers):
+        self.signers = signers
+    def __str__(self):
+        return ", ".join("{}: {}".format(s.fpr,
+                                         pygpgme.gpgme_strerror(s.reason))
+                         for s in self.signers)
+
+class VerificationError(PymeError):
+    pass
+
+class BadSignatures(VerificationError):
+    def __init__(self, result):
+        self.result = result
+    def __str__(self):
+        return ", ".join("{}: {}".format(s.fpr,
+                                         pygpgme.gpgme_strerror(s.status))
+                         for s in self.result.signatures
+                         if s.status != NO_ERROR)
+
+class MissingSignatures(VerificationError):
+    def __init__(self, result, missing):
+        self.result = result
+        self.missing = missing
+    def __str__(self):
+        return ", ".join(k.subkeys[0].fpr for k in self.missing)
index 5527a1a..bbd28fe 100644 (file)
@@ -1,3 +1,4 @@
+# Copyright (C) 2016 g10 Code GmbH
 # Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net>
 # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org>
 #
 
 from . import pygpgme
 
-def process_constants(starttext, dict):
-    """Called by the constant libraries to load up the appropriate constants
-    from the C library."""
-    index = len(starttext)
-    for identifier in dir(pygpgme):
-        if not identifier.startswith(starttext):
-            continue
-        name = identifier[index:]
-        dict[name] = getattr(pygpgme, identifier)
+def process_constants(prefix, scope):
+    """Called by the constant modules to load up the constants from the C
+    library starting with PREFIX.  Matching constants will be inserted
+    into SCOPE with PREFIX stripped from the names.  Returns the names
+    of inserted constants.
+
+    """
+    index = len(prefix)
+    constants = {identifier[index:]: getattr(pygpgme, identifier)
+                 for identifier in dir(pygpgme)
+                 if identifier.startswith(prefix)}
+    scope.update(constants)
+    return list(constants.keys())
index 4a206fd..b2e725f 100644 (file)
@@ -52,7 +52,7 @@ py_tests = t-wrapper.py \
        t-idiomatic.py
 
 TESTS = initial.py $(py_tests) final.py
-EXTRA_DIST = support.py $(TESTS)
+EXTRA_DIST = support.py $(TESTS) encrypt-only.asc sign-only.asc
 
 CLEANFILES = secring.gpg pubring.gpg pubring.kbx trustdb.gpg dirmngr.conf \
        gpg-agent.conf pubring.kbx~ gpg.conf pubring.gpg~ \
diff --git a/lang/python/tests/encrypt-only.asc b/lang/python/tests/encrypt-only.asc
new file mode 100644 (file)
index 0000000..6e068a0
--- /dev/null
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+lQPGBFd/jL0BCAD8jfoblIrlHS0shDCbSiO7RFaT6sEa/6tSPkv6XzBba9oXOkuO
+FLTkNpIwPb92U8SOS+27j7n9v6U5NW2tyZwIoeLb8lUyKnCBr22IUhTFVXf7fros
+zmPugsJaDBi9f7RL0bqiCn4EV3DGKyAukZklk1k1JV4Ec3dEPMAmL9LmnvXreEjU
+pQZZN9sJV32ew8CYkZ6AB8foFQwfxn4x0iUoKvj8kW9RsY1KMPucp4YiFhHeMZW1
+5wGAZdEIZYKyWEp4bi/wC9yn/TUR5uNWc0uVJzQvuHwaYjolPW89DinjBkPEJCBr
+RwumaOWfbu/hb51wBoUTmUr9diVw93L2ROLPABEBAAH+BwMC1bmUAoPJKI/WBiHm
+P6tSNRLdd+7etfjAFvKL7Ob2pNTrc3hbtyOLIQ9tuEaqXEyfnCms/DCg8QdkaFUv
+Nkoj0W5+G/MQuR2jIvrq/wyL/4jIw0AFbp9/V1JbSXZh2g1eJLnnykn7uPxCbDFY
+FrVeFmkhoxZ3pid6ZQSWlxXsdW+YMvbUfNIIZpbygI/alIBvbDS1YJYEBDCwFZjU
+7quE2Ufxo8dm34EHcmbpYpn4r3DUrU5AHQ2fIprLIVqHn4+NUrR8WZS9nCnIeu/z
+OaJUZ2lJFRjUC6Gpsbsw6Xwh4Ntwzyt2SsXc+UVZngjozw3yw0VpDifxMBqcd+9x
+baSc7dfbOZF2BCZOwnB7/QrFZDaqe5b3n6rTdj1va/CrJMuxbgaNAjvLpdT2EUPZ
+fHDAdPAjASofxBREv+HIKwksuPJ9cvavZU6Q4KQA7buo25hd7yjuba4WbLQhp0jH
+AT1P7SdakMhk/IFcUKFdB3ZyZZZ1JTTPa2xZn9yDa3Jb1t7IMLYLwY6EFbjvaxH5
+WEGZvOAq2iEa941mxv4miwgf7MQPx6g9u0+dXc7iZApwWs9MNfJo3J25sKhWK5Be
+Bu3w7c6nrlg40GtPuDRgaBvYWbVerJcepTA/EPfugEJtRsDJkt7wZq1H9lWHU7Ih
+Up6/+XKtBzlCIqYjorzFLnC721pcKFcPhLgvtjjNJvUsLXbr9CwnBub/eTFcfRb2
+ro60H9cOhf0fQSQyvkZWfzq0BN6rG27G1KhyprsJAmpW0fTHHkB4V19788C2sTQv
+D93VU3Nd6MWocwAYtPWmtwXPpuOAU9IcwAvVTxBeBJCXxbH3uyx1frwDXA7lf4Pb
+a8hMoMMVU+rAG1uepKI5h4seBIKP7qKEKAPloI6/Vtf7/Ump4DKprS1QpfOW+lsX
+aR48lgNR6sQXtDdFbmNyeXB0aW9uIE9ubHkgKHRlc3Qga2V5LCBkbyBub3QgdXNl
+KSA8ZW9AZXhhbXBsZS5vcmc+iQE3BBMBCAAhBQJXf4y9AhsNBQsJCAcCBhUICQoL
+AgQWAgMBAh4BAheAAAoJEJIFcnabn+Gc/KgH/07wzrsBzTqdI5L6cIqQ81Vq8ASj
+tsuYoVfFxymB8F/AxpnLMhYRuWQTcoUHQ/olG2yA0C6o4e1JPAmh6LQGwr0eRnc2
+2tr4cbnQAhXpJ8xOR6kH9eE8nGeC7tlEeeV/Wnj3SLZOXOjYjnA9bA3JX9DP3qcz
+w1sKQPEHsGkMJuT0ZadnlJ1qw8AnnNKLDlG4kIO9hz3qB8BjxFZf+j5f/nhFNv5I
+pnNdMcDwQqHVrwD6WO+Xmmdykab0awL9To0S9DG9ohcXuJiTMa8vtXFSBM0koUDk
+BWajEq+QAcDpmdFsQr4/gbzvHkAIVTQb0seJr4gpmXFZu3TMuGVD9j13GaI=
+=38ri
+-----END PGP PRIVATE KEY BLOCK-----
index 9d72cbc..169c3df 100755 (executable)
 
 import os
 import subprocess
+import pyme
+import support
+support.init_gpgme(pyme.constants.PROTOCOL_OpenPGP)
 
 subprocess.check_call([os.path.join(os.getenv('top_srcdir'),
                                     "tests", "start-stop-agent"), "--start"])
+
+with pyme.Context() as c:
+    alpha = c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False)
+    bob = c.get_key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", False)
+
+    # Mark alpha as trusted.  The signature verification tests expect
+    # this.
+    support.mark_key_trusted(c, alpha)
+
+    c.op_import(open(support.in_srcdir("encrypt-only.asc")))
+    c.op_import(open(support.in_srcdir("sign-only.asc")))
diff --git a/lang/python/tests/sign-only.asc b/lang/python/tests/sign-only.asc
new file mode 100644 (file)
index 0000000..6e2a6f3
--- /dev/null
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v2
+
+lQPFBFd/jO8BCADiull4EVJiKmJqclPyU6GhTlbJXw7Ch0zbFAauOWYT3ACmgr1U
+KfJlZ2sPe2EezZkVSACxgIjTCzcgKQLh/swXdhO8uEgWEIN8f07WcSVDrcRGYwDS
+KFSRsK0bfO/OQQDUsSkNQSHjcOdLnCHCinMrQi1mBZOs+Y/DXOkkEV1zbFFV7q6X
+4vX9HSWwTRQTdOV9CFZykbwM+X1YIZlVtpOAKqSNJi3P17uQF7P9zko6HWKKKQ5S
+96BfXUOIpBRl82R85/yQgeGrWlvZ2BT2ittscNQlBKqLHJ7LIeDr9ctbKlKZjHTn
+Da7NYg+PoMHspbizjSONbEzpcR/9ZUq16oJJABEBAAH+BwMC7hQZNJSmlX/W6sfL
+0wakX6kTsiCEMy2vMCRcZ769JKT234avHtkL/g7MBJEzqdG9HSEp7+LHGuOWJhfa
+20f61WvPT5ujUIy//QXJ9a8z877jCm+fHKCTDXGYLLfCkJLfr3/GfTRy6gaIGTSw
+BqZaRelPvHbMp+eiFqDkf8W/E1LO3/83k87+pXggjz4p0OasyMw8RcDmy+IKBMGG
+bzet5WIKHIhpblIzuuucQHOjtwA8vCedub3F4lcRuULe2GW6sNuCB9kjSC9g6D1d
+bJ+WYi5GiUUQARGSNXiWVoVPLpEo0i6/2bKJ7vBYGRewNp42ebVQU2bFW7uzhaIq
+4itzNTjFNTpcxX3Lo0/mzJpe7pVRJwN+HGahNGT0EtPDsT/nNTFDUq8e8nt0U9/8
+0eekg4MRBJEzE3A+wosIHPjzCkQgu98+nh79rPMbCpZVxNfLb136cTkubmHCWptN
+T2MbqK2L4hMcOxHGGOmI9SjFltNeKtTsVtkxh3Vj67UESPdN550centfasJYA0bj
+guRQfHWHJXYIfFwblIFkl8xtIVLTeWlQMEvc7oI8jcJOc2ri8Zdjj/55xxv/RvjC
+ZKzfjPpdkLYcN1zP/hETLD68u7WmiMAYCr8Eq9YQ3oKklUpWxRMCAAtmgjGGpm5P
+QQW+36s96Q3cuG8R0Z4Wo8y89FgWzCEzuAhemCdffoUA8kn0HJQaVndnExJb1Ebz
+wp+zsX/JqiOFvcKHJAWCaXkk0oXVi1aIV4tQyCPfhyjnd846K7g8UabAz51IJHvF
+CXRAmqJvu26NqjYOfWBJJxZQsPH4FjPfYx+e/MFPZa+UTKCfzaOHClrePHUDHw58
+Ez5ItcORYn51IWW33r+c4tlhW5mrjMD7FcjFOuYT4EIivd5BSnwLP0fjBz8TBVAY
+yyFO+YAXTQ+0MVNpZ24gT25seSAodGVzdCBrZXksIGRvIG5vdCB1c2UpIDxzb0Bl
+eGFtcGxlLm9yZz6JATcEEwEIACEFAld/jO8CGwMFCwkIBwIGFQgJCgsCBBYCAwEC
+HgECF4AACgkQ/tFT8S8Y9F3PAwgAvKav6+luvcAhrpBMO4z/Q8kDMtO5AW1KTEcz
+neqpj5eTVJVbYUgDuBlEXbFYtcZmYyYtJC5KQkN3bxPmehVUzGk27UYWMWbPIWyU
+riGcFL5BWWQaKSqiWUypzhNVnxYoiWVhHeJ36LICVMpLBaubgcpwCSW/j58yZo/7
+XRwf40OblXr4cevIW4Oq5GSxKOQF+DCErF6BeikC2i+NoqSxwNiIO/1NUxs8QfAI
+z8UT/bSUXr62BWLfeCIDGgXutMMPth3tKi4DlvLCzI6eYJrd8E3Rt7iUZm9IH8OQ
+Djv2DKnL/E/AP8oITItrOmICqfEWcj+Tk2Xep4pCCMNU+Pa0yg==
+=gG5b
+-----END PGP PRIVATE KEY BLOCK-----
index 8bafea8..f42fc2e 100644 (file)
@@ -19,14 +19,48 @@ import sys
 import os
 from pyme import core
 
+# known keys
+alpha = "A0FF4590BB6122EDEF6E3C542D727CC768697734"
+bob = "D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2"
+encrypt_only = "F52770D5C4DB41408D918C9F920572769B9FE19C"
+sign_only = "7CCA20CCDE5394CEE71C9F0BFED153F12F18F45D"
+
 def make_filename(name):
     return os.path.join(os.environ['top_srcdir'], 'tests', 'gpg', name)
 
+def in_srcdir(name):
+    return os.path.join(os.environ['srcdir'], name)
+
 def init_gpgme(proto):
     core.engine_check_version(proto)
 
 verbose = int(os.environ.get('verbose', 0)) > 1
 def print_data(data):
     if verbose:
-        data.seek(0, os.SEEK_SET)
-        sys.stdout.buffer.write(data.read())
+        try:
+            # See if it is a file-like object.
+            data.seek(0, os.SEEK_SET)
+            data = data.read()
+        except:
+            # Hope for the best.
+            pass
+        sys.stdout.buffer.write(data)
+
+def mark_key_trusted(ctx, key):
+    class Editor(object):
+        def __init__(self):
+            self.steps = ["trust", "save"]
+        def edit(self, status, args, out):
+            if args == "keyedit.prompt":
+                result = self.steps.pop(0)
+            elif args == "edit_ownertrust.value":
+                result = "5"
+            elif args == "edit_ownertrust.set_ultimate.okay":
+                result = "Y"
+            elif args == "keyedit.save.okay":
+                result = "Y"
+            else:
+                result = None
+            return result
+    with core.Data() as sink:
+        ctx.op_edit(key, Editor().edit, sink, sink)
index 433e0a1..0f615dc 100755 (executable)
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import pyme
 from pyme import core, constants, errors
 import support
 
@@ -28,7 +29,7 @@ def check_verify_result(result, summary, fpr, status):
     assert errors.GPGMEError(sig.status).getcode() == status
     assert len(sig.notations) == 0
     assert not sig.wrong_key_usage
-    assert sig.validity == constants.VALIDITY_UNKNOWN
+    assert sig.validity == constants.VALIDITY_FULL
     assert errors.GPGMEError(sig.validity_reason).getcode() == errors.NO_ERROR
 
 support.init_gpgme(constants.PROTOCOL_OpenPGP)
@@ -45,6 +46,29 @@ assert not result.unsupported_algorithm, \
 support.print_data(sink)
 
 verify_result = c.op_verify_result()
-check_verify_result(verify_result, 0,
+check_verify_result(verify_result,
+                    constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
                     "A0FF4590BB6122EDEF6E3C542D727CC768697734",
                     errors.NO_ERROR)
+
+# Idiomatic interface.
+with pyme.Context() as c:
+    alpha = c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False)
+    bob = c.get_key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", False)
+    plaintext, _, verify_result = \
+        c.decrypt(open(support.make_filename("cipher-2.asc")), verify=[alpha])
+    assert plaintext.find(b'Wenn Sie dies lesen k') >= 0, \
+        'Plaintext not found'
+    check_verify_result(verify_result,
+                        constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
+                        "A0FF4590BB6122EDEF6E3C542D727CC768697734",
+                        errors.NO_ERROR)
+
+    try:
+        c.decrypt(open(support.make_filename("cipher-2.asc")),
+                  verify=[alpha, bob])
+    except errors.MissingSignatures as e:
+        assert len(e.missing) == 1
+        assert e.missing[0] == bob
+    else:
+        assert False, "Expected an error, got none"
index bd7b59f..b5c4700 100755 (executable)
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import pyme
 from pyme import core, constants
 import support
 
@@ -32,3 +33,10 @@ assert not result.unsupported_algorithm, \
     "Unsupported algorithm: {}".format(result.unsupported_algorithm)
 
 support.print_data(sink)
+
+# Idiomatic interface.
+with pyme.Context() as c:
+    plaintext, _, _ = c.decrypt(open(support.make_filename("cipher-1.asc")))
+    assert len(plaintext) > 0
+    assert plaintext.find(b'Wenn Sie dies lesen k') >= 0, \
+        'Plaintext not found'
index cba697c..31cc94f 100755 (executable)
@@ -18,6 +18,7 @@
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import pyme
 from pyme import core, constants
 import support
 
@@ -69,3 +70,26 @@ for recipients in (keys, []):
     check_result(result, constants.SIG_MODE_NORMAL)
 
     support.print_data(sink)
+
+
+# Idiomatic interface.
+with pyme.Context(armor=True) as c:
+    message = "Hallo Leute\n".encode()
+    ciphertext, _, sig_result = c.encrypt(message,
+                                          recipients=keys,
+                                          always_trust=True)
+    assert len(ciphertext) > 0
+    assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found'
+    check_result(sig_result, constants.SIG_MODE_NORMAL)
+
+    c.signers = [c.get_key(support.sign_only, True)]
+    c.encrypt(message, recipients=keys, always_trust=True)
+
+    c.signers = [c.get_key(support.encrypt_only, True)]
+    try:
+        c.encrypt(message, recipients=keys, always_trust=True)
+    except pyme.errors.InvalidSigners as e:
+        assert len(e.signers) == 1
+        assert support.encrypt_only.endswith(e.signers[0].fpr)
+    else:
+        assert False, "Expected an InvalidSigners error, got none"
index 0b24fd5..c5be183 100755 (executable)
@@ -18,6 +18,7 @@
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import os
+import pyme
 from pyme import core, constants
 import support
 
@@ -61,3 +62,22 @@ for passphrase in ("abc", b"abc"):
     plaintext = plain.read()
     assert plaintext == b"Hallo Leute\n", \
         "Wrong plaintext {!r}".format(plaintext)
+
+# Idiomatic interface.
+for passphrase in ("abc", b"abc"):
+    with pyme.Context(armor=True) as c:
+        # Check that the passphrase callback is not altered.
+        def f(*args):
+            assert False
+        c.set_passphrase_cb(f)
+
+        message = "Hallo Leute\n".encode()
+        ciphertext, _, _ = c.encrypt(message,
+                                     passphrase=passphrase,
+                                     sign=False)
+        assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found'
+
+        plaintext, _, _ = c.decrypt(ciphertext, passphrase=passphrase)
+        assert plaintext == message, 'Message body not recovered'
+
+        assert c._passphrase_cb[1] == f, "Passphrase callback not restored"
index 24869fc..4c77f39 100755 (executable)
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
+import pyme
 from pyme import core, constants
 import support
 
@@ -34,6 +35,28 @@ keys.append(c.get_key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", False))
 c.op_encrypt(keys, constants.ENCRYPT_ALWAYS_TRUST, source, sink)
 result = c.op_encrypt_result()
 assert not result.invalid_recipients, \
-    "Invalid recipient encountered: {}".format(result.invalid_recipients.fpr)
-
+  "Invalid recipients: {}".format(", ".join(r.fpr for r in result.recipients))
 support.print_data(sink)
+
+# Idiomatic interface.
+with pyme.Context(armor=True) as c:
+    ciphertext, _, _ = c.encrypt("Hallo Leute\n".encode(),
+                                 recipients=keys,
+                                 sign=False,
+                                 always_trust=True)
+    assert len(ciphertext) > 0
+    assert ciphertext.find(b'BEGIN PGP MESSAGE') > 0, 'Marker not found'
+
+    c.encrypt("Hallo Leute\n".encode(),
+              recipients=[c.get_key(support.encrypt_only, False)],
+              sign=False, always_trust=True)
+
+    try:
+        c.encrypt("Hallo Leute\n".encode(),
+                  recipients=[c.get_key(support.sign_only, False)],
+                  sign=False, always_trust=True)
+    except pyme.errors.InvalidRecipients as e:
+        assert len(e.recipients) == 1
+        assert support.sign_only.endswith(e.recipients[0].fpr)
+    else:
+        assert False, "Expected an InvalidRecipients error, got none"
index b252690..1989c92 100755 (executable)
 import io
 import os
 import tempfile
-from pyme import core, constants, errors
+import pyme
 import support
 
-support.init_gpgme(constants.PROTOCOL_OpenPGP)
+support.init_gpgme(pyme.constants.PROTOCOL_OpenPGP)
 
 # Both Context and Data can be used as context manager:
-with core.Context() as c, core.Data() as d:
+with pyme.Context() as c, pyme.Data() as d:
     c.get_engine_info()
     d.write(b"Halloechen")
     leak_c = c
@@ -35,16 +35,17 @@ assert leak_c.wrapped == None
 assert leak_d.wrapped == None
 
 def sign_and_verify(source, signed, sink):
-    with core.Context() as c:
-        c.op_sign(source, signed, constants.SIG_MODE_NORMAL)
+    with pyme.Context() as c:
+        c.op_sign(source, signed, pyme.constants.SIG_MODE_NORMAL)
         signed.seek(0, os.SEEK_SET)
         c.op_verify(signed, None, sink)
         result = c.op_verify_result()
 
     assert len(result.signatures) == 1, "Unexpected number of signatures"
     sig = result.signatures[0]
-    assert sig.summary == 0
-    assert errors.GPGMEError(sig.status).getcode() == errors.NO_ERROR
+    assert sig.summary == (pyme.constants.SIGSUM_VALID |
+                           pyme.constants.SIGSUM_GREEN)
+    assert pyme.errors.GPGMEError(sig.status).getcode() == pyme.errors.NO_ERROR
 
     sink.seek(0, os.SEEK_SET)
     assert sink.read() == b"Hallo Leute\n"
@@ -71,5 +72,5 @@ else:
 
 # Demonstrate automatic wrapping of objects implementing the buffer
 # interface, and the use of data objects with the 'with' statement.
-with io.BytesIO(preallocate) as signed, core.Data() as sink:
+with io.BytesIO(preallocate) as signed, pyme.Data() as sink:
     sign_and_verify(b"Hallo Leute\n", signed, sink)
index ee9c283..64fec27 100755 (executable)
@@ -115,8 +115,15 @@ def check_global(key, uids, n_subkeys):
         "Key unexpectedly carries issuer name: {}".format(key.issuer_name)
     assert not key.chain_id, \
         "Key unexpectedly carries chain ID: {}".format(key.chain_id)
-    assert key.owner_trust == constants.VALIDITY_UNKNOWN, \
+
+    # Only key Alfa is trusted
+    assert key.uids[0].name == 'Alfa Test' \
+      or key.owner_trust == constants.VALIDITY_UNKNOWN, \
+        "Key has unexpected owner trust: {}".format(key.owner_trust)
+    assert key.uids[0].name != 'Alfa Test' \
+      or key.owner_trust == constants.VALIDITY_ULTIMATE, \
         "Key has unexpected owner trust: {}".format(key.owner_trust)
+
     assert len(key.subkeys) - 1 == n_subkeys, \
         "Key `{}' has unexpected number of subkeys".format(uids[0][0])
 
@@ -161,7 +168,10 @@ def check_subkey(fpr, which, subkey):
 def check_uid(which, ref, uid):
     assert not uid.revoked, which + " user ID unexpectedly revoked"
     assert not uid.invalid, which + " user ID unexpectedly invalid"
-    assert uid.validity == constants.VALIDITY_UNKNOWN, \
+    assert uid.validity == (constants.VALIDITY_UNKNOWN
+                            if uid.name.split()[0]
+                            not in {'Alfa', 'Alpha', 'Alice'} else
+                            constants.VALIDITY_ULTIMATE), \
       which + " user ID has unexpectedly validity: {}".format(uid.validity)
     assert not uid.signatures, which + " user ID unexpectedly signed"
     assert uid.name == ref[0], \
index a721f03..802a32d 100755 (executable)
 
 import sys
 import os
+import pyme
 from pyme import core, constants
 import support
 
+def fail(msg):
+    raise RuntimeError(msg)
+
 def check_result(r, typ):
     if r.invalid_signers:
-        sys.exit("Invalid signer found: {}".format(r.invalid_signers.fpr))
+        fail("Invalid signer found: {}".format(r.invalid_signers.fpr))
 
     if len(r.signatures) != 1:
-        sys.exit("Unexpected number of signatures created")
+        fail("Unexpected number of signatures created")
 
     signature = r.signatures[0]
     if signature.type != typ:
-        sys.exit("Wrong type of signature created")
+        fail("Wrong type of signature created")
 
     if signature.pubkey_algo != constants.PK_DSA:
-        sys.exit("Wrong pubkey algorithm reported: {}".format(
+        fail("Wrong pubkey algorithm reported: {}".format(
             signature.pubkey_algo))
 
     if signature.hash_algo != constants.MD_SHA1:
-        sys.exit("Wrong hash algorithm reported: {}".format(
+        fail("Wrong hash algorithm reported: {}".format(
             signature.hash_algo))
 
     if signature.sig_class != 1:
-        sys.exit("Wrong signature class reported: {}".format(
+        fail("Wrong signature class reported: {}".format(
             signature.sig_class))
 
     if signature.fpr != "A0FF4590BB6122EDEF6E3C542D727CC768697734":
-        sys.exit("Wrong fingerprint reported: {}".format(signature.fpr))
+        fail("Wrong fingerprint reported: {}".format(signature.fpr))
 
 
 support.init_gpgme(constants.PROTOCOL_OpenPGP)
@@ -82,3 +86,35 @@ c.op_sign(source, sink, constants.SIG_MODE_CLEAR)
 result = c.op_sign_result()
 check_result(result, constants.SIG_MODE_CLEAR)
 support.print_data(sink)
+
+# Idiomatic interface.
+with pyme.Context(armor=True, textmode=True) as c:
+    message = "Hallo Leute\n".encode()
+    signed, _ = c.sign(message)
+    assert len(signed) > 0
+    assert signed.find(b'BEGIN PGP MESSAGE') > 0, 'Message not found'
+
+    signed, _ = c.sign(message, mode=pyme.constants.SIG_MODE_DETACH)
+    assert len(signed) > 0
+    assert signed.find(b'BEGIN PGP SIGNATURE') > 0, 'Signature not found'
+
+    signed, _ = c.sign(message, mode=pyme.constants.SIG_MODE_CLEAR)
+    assert len(signed) > 0
+    assert signed.find(b'BEGIN PGP SIGNED MESSAGE') > 0, 'Message not found'
+    assert signed.find(message) > 0, 'Message content not found'
+    assert signed.find(b'BEGIN PGP SIGNATURE') > 0, 'Signature not found'
+
+with pyme.Context() as c:
+    message = "Hallo Leute\n".encode()
+
+    c.signers = [c.get_key(support.sign_only, True)]
+    c.sign(message)
+
+    c.signers = [c.get_key(support.encrypt_only, True)]
+    try:
+        c.sign(message)
+    except pyme.errors.InvalidSigners as e:
+        assert len(e.signers) == 1
+        assert support.encrypt_only.endswith(e.signers[0].fpr)
+    else:
+        assert False, "Expected an InvalidSigners error, got none"
index 26dded5..15e8011 100755 (executable)
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import pyme
 from pyme import core, constants
 import support
 
+def fail(msg):
+    raise RuntimeError(msg)
+
 def check_result(r, typ):
     if r.invalid_signers:
-        sys.exit("Invalid signer found: {}".format(r.invalid_signers.fpr))
+        fail("Invalid signer found: {}".format(r.invalid_signers.fpr))
 
     if len(r.signatures) != 2:
-        sys.exit("Unexpected number of signatures created")
+        fail("Unexpected number of signatures created")
 
     for signature in r.signatures:
         if signature.type != typ:
-            sys.exit("Wrong type of signature created")
+            fail("Wrong type of signature created")
 
         if signature.pubkey_algo != constants.PK_DSA:
-            sys.exit("Wrong pubkey algorithm reported: {}".format(
+            fail("Wrong pubkey algorithm reported: {}".format(
                 signature.pubkey_algo))
 
         if signature.hash_algo != constants.MD_SHA1:
-            sys.exit("Wrong hash algorithm reported: {}".format(
+            fail("Wrong hash algorithm reported: {}".format(
                 signature.hash_algo))
 
         if signature.sig_class != 1:
-            sys.exit("Wrong signature class reported: {}".format(
-                signature.sig_class))
+            fail("Wrong signature class reported: got {}, want {}".format(
+                signature.sig_class, 1))
 
         if signature.fpr not in ("A0FF4590BB6122EDEF6E3C542D727CC768697734",
                                  "23FD347A419429BACCD5E72D6BC4778054ACD246"):
-            sys.exit("Wrong fingerprint reported: {}".format(signature.fpr))
+            fail("Wrong fingerprint reported: {}".format(signature.fpr))
 
 
 support.init_gpgme(constants.PROTOCOL_OpenPGP)
@@ -73,3 +77,20 @@ for mode in (constants.SIG_MODE_NORMAL, constants.SIG_MODE_DETACH,
     result = c.op_sign_result()
     check_result(result, mode)
     support.print_data(sink)
+
+# Idiomatic interface.
+with pyme.Context(armor=True, textmode=True, signers=keys) as c:
+    message = "Hallo Leute\n".encode()
+    signed, result = c.sign(message)
+    check_result(result, constants.SIG_MODE_NORMAL)
+    assert signed.find(b'BEGIN PGP MESSAGE') > 0, 'Message not found'
+
+    signed, result = c.sign(message, mode=constants.SIG_MODE_DETACH)
+    check_result(result, constants.SIG_MODE_DETACH)
+    assert signed.find(b'BEGIN PGP SIGNATURE') > 0, 'Signature not found'
+
+    signed, result = c.sign(message, mode=constants.SIG_MODE_CLEAR)
+    check_result(result, constants.SIG_MODE_CLEAR)
+    assert signed.find(b'BEGIN PGP SIGNED MESSAGE') > 0, 'Message not found'
+    assert signed.find(message) > 0, 'Message content not found'
+    assert signed.find(b'BEGIN PGP SIGNATURE') > 0, 'Signature not found'
index 333ee4e..b88bd07 100755 (executable)
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import os
+import pyme
 from pyme import core, constants, errors
 import support
 
-test_text1 = "Just GNU it!\n"
-test_text1f= "Just GNU it?\n"
-test_sig1 = """-----BEGIN PGP SIGNATURE-----
+test_text1 = b"Just GNU it!\n"
+test_text1f= b"Just GNU it?\n"
+test_sig1 = b"""-----BEGIN PGP SIGNATURE-----
 
 iN0EABECAJ0FAjoS+i9FFIAAAAAAAwA5YmFyw7bDpMO8w58gZGFzIHdhcmVuIFVt
 bGF1dGUgdW5kIGpldHp0IGVpbiBwcm96ZW50JS1aZWljaGVuNRSAAAAAAAgAJGZv
@@ -34,7 +35,7 @@ dADGKXF/Hcb+AKCJWPphZCphduxSvrzH0hgzHdeQaA==
 -----END PGP SIGNATURE-----
 """
 
-test_sig2 = """-----BEGIN PGP MESSAGE-----
+test_sig2 = b"""-----BEGIN PGP MESSAGE-----
 
 owGbwMvMwCSoW1RzPCOz3IRxjXQSR0lqcYleSUWJTZOvjVdpcYmCu1+oQmaJIleH
 GwuDIBMDGysTSIqBi1MApi+nlGGuwDeHao53HBr+FoVGP3xX+kvuu9fCMJvl6IOf
@@ -44,7 +45,7 @@ y1kvP4y+8D5a11ang0udywsA
 """
 
 # A message with a prepended but unsigned plaintext packet.
-double_plaintext_sig = """-----BEGIN PGP MESSAGE-----
+double_plaintext_sig = b"""-----BEGIN PGP MESSAGE-----
 
 rDRiCmZvb2Jhci50eHRF4pxNVGhpcyBpcyBteSBzbmVha3kgcGxhaW50ZXh0IG1l
 c3NhZ2UKowGbwMvMwCSoW1RzPCOz3IRxTWISa6JebnG666MFD1wzSzJSixQ81XMV
@@ -55,10 +56,12 @@ UqVooWlGXHwNw/xg/fVzt9VNbtjtJ/fhUqYo0/LyCGEA
 -----END PGP MESSAGE-----
 """
 
-def check_result(result, summary, fpr, status, notation):
+def check_result(result, summary, validity, fpr, status, notation):
     assert len(result.signatures) == 1, "Unexpected number of signatures"
     sig = result.signatures[0]
-    assert sig.summary == summary, "Unexpected signature summary"
+    assert sig.summary == summary, \
+        "Unexpected signature summary: {}, want: {}".format(sig.summary,
+                                                            summary)
     assert sig.fpr == fpr
     assert errors.GPGMEError(sig.status).getcode() == status
 
@@ -83,7 +86,9 @@ def check_result(result, summary, fpr, status, notation):
         assert len(expected_notations) == 0
 
     assert not sig.wrong_key_usage
-    assert sig.validity == constants.VALIDITY_UNKNOWN
+    assert sig.validity == validity, \
+        "Unexpected signature validity: {}, want: {}".format(
+            sig.validity, validity)
     assert errors.GPGMEError(sig.validity_reason).getcode() == errors.NO_ERROR
 
 
@@ -96,7 +101,9 @@ text = core.Data(test_text1)
 sig = core.Data(test_sig1)
 c.op_verify(sig, text, None)
 result = c.op_verify_result()
-check_result(result, 0, "A0FF4590BB6122EDEF6E3C542D727CC768697734",
+check_result(result, constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
+             constants.VALIDITY_FULL,
+             "A0FF4590BB6122EDEF6E3C542D727CC768697734",
              errors.NO_ERROR, True)
 
 
@@ -105,15 +112,17 @@ text = core.Data(test_text1f)
 sig.seek(0, os.SEEK_SET)
 c.op_verify(sig, text, None)
 result = c.op_verify_result()
-check_result(result, constants.SIGSUM_RED, "2D727CC768697734",
-             errors.BAD_SIGNATURE, False)
+check_result(result, constants.SIGSUM_RED, constants.VALIDITY_UNKNOWN,
+             "2D727CC768697734", errors.BAD_SIGNATURE, False)
 
 # Checking a normal signature.
 text = core.Data()
 sig = core.Data(test_sig2)
 c.op_verify(sig, None, text)
 result = c.op_verify_result()
-check_result(result, 0, "A0FF4590BB6122EDEF6E3C542D727CC768697734",
+check_result(result, constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
+             constants.VALIDITY_FULL,
+             "A0FF4590BB6122EDEF6E3C542D727CC768697734",
              errors.NO_ERROR, False)
 
 # Checking an invalid message.
@@ -126,3 +135,54 @@ except Exception as e:
     assert e.getcode() == errors.BAD_DATA
 else:
     assert False, "Expected an error but got none."
+
+
+# Idiomatic interface.
+with pyme.Context(armor=True) as c:
+    # Checking a valid message.
+    _, result = c.verify(test_text1, test_sig1)
+    check_result(result, constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
+                 constants.VALIDITY_FULL,
+                 "A0FF4590BB6122EDEF6E3C542D727CC768697734",
+                 errors.NO_ERROR, True)
+
+    # Checking a manipulated message.
+    try:
+        c.verify(test_text1f, test_sig1)
+    except errors.BadSignatures as e:
+        check_result(e.result, constants.SIGSUM_RED,
+                     constants.VALIDITY_UNKNOWN,
+                     "2D727CC768697734", errors.BAD_SIGNATURE, False)
+    else:
+        assert False, "Expected an error but got none."
+
+    # Checking a normal signature.
+    sig = core.Data(test_sig2)
+    data, result = c.verify(test_sig2)
+    check_result(result, constants.SIGSUM_VALID | constants.SIGSUM_GREEN,
+                 constants.VALIDITY_FULL,
+                 "A0FF4590BB6122EDEF6E3C542D727CC768697734",
+                 errors.NO_ERROR, False)
+    assert data == test_text1
+
+    # Checking an invalid message.
+    try:
+        c.verify(double_plaintext_sig)
+    except errors.GPGMEError as e:
+        assert e.getcode() == errors.BAD_DATA
+    else:
+        assert False, "Expected an error but got none."
+
+    alpha = c.get_key("A0FF4590BB6122EDEF6E3C542D727CC768697734", False)
+    bob = c.get_key("D695676BDCEDCC2CDD6152BCFE180B1DA9E3B0B2", False)
+
+    # Checking a valid message.
+    c.verify(test_text1, test_sig1, verify=[alpha])
+
+    try:
+        c.verify(test_text1, test_sig1, verify=[alpha, bob])
+    except errors.MissingSignatures as e:
+        assert len(e.missing) == 1
+        assert e.missing[0] == bob
+    else:
+        assert False, "Expected an error, got none"