[git] GPGME - branch, master, updated. gpgme-1.6.0-239-ge545ca3

by Justus Winter cvs at cvs.gnupg.org
Fri Jul 15 18:31:20 CEST 2016


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "GnuPG Made Easy".

The branch, master has been updated
       via  e545ca3f682b615ce447c186a2c73489a32b0592 (commit)
       via  1f318b7aaaa77672fab117d54fe75221780df83c (commit)
      from  d2f2cbd297c4d2f2c3264ecc3ffb7a2b3a70bd55 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit e545ca3f682b615ce447c186a2c73489a32b0592
Author: Justus Winter <justus at g10code.com>
Date:   Fri Jul 15 18:27:04 2016 +0200

    python: Make GPGME's version easily accessible.
    
    * lang/python/pyme/version.py.in (gpgme_versionstr): New variable.
    
    Signed-off-by: Justus Winter <justus at g10code.com>

diff --git a/lang/python/pyme/version.py.in b/lang/python/pyme/version.py.in
index 764bf69..cfa49a2 100644
--- a/lang/python/pyme/version.py.in
+++ b/lang/python/pyme/version.py.in
@@ -16,12 +16,11 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 
+from . import pygpgme
+
 productname = 'pyme'
 versionstr = "@VERSION@"
-
-# XXX: Do we want to embed such information?
-#revno = int('$Rev: 281 $'[6:-2])
-#revstr = "Rev %d" % revno
+gpgme_versionstr = pygpgme.GPGME_VERSION
 
 versionlist = versionstr.split(".")
 major = versionlist[0]

commit 1f318b7aaaa77672fab117d54fe75221780df83c
Author: Justus Winter <justus at g10code.com>
Date:   Wed Jun 8 18:58:57 2016 +0200

    python: Add an idiomatic interface.
    
    * 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 at g10code.com>

diff --git a/configure.ac b/configure.ac
index d395e00..6a7df24 100644
--- a/configure.ac
+++ b/configure.ac
@@ -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
diff --git a/lang/python/pyme/__init__.py b/lang/python/pyme/__init__.py
index e377f59..c42f794 100644
--- a/lang/python/pyme/__init__.py
+++ b/lang/python/pyme/__init__.py
@@ -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
diff --git a/lang/python/pyme/core.py b/lang/python/pyme/core.py
index e5ccf7c..6ca8cb8 100644
--- a/lang/python/pyme/core.py
+++ b/lang/python/pyme/core.py
@@ -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:
diff --git a/lang/python/pyme/errors.py b/lang/python/pyme/errors.py
index f96877b..0194931 100644
--- a/lang/python/pyme/errors.py
+++ b/lang/python/pyme/errors.py
@@ -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)
diff --git a/lang/python/pyme/util.py b/lang/python/pyme/util.py
index 5527a1a..bbd28fe 100644
--- a/lang/python/pyme/util.py
+++ b/lang/python/pyme/util.py
@@ -1,3 +1,4 @@
+# Copyright (C) 2016 g10 Code GmbH
 # Copyright (C) 2004,2008 Igor Belyi <belyi at users.sourceforge.net>
 # Copyright (C) 2002 John Goerzen <jgoerzen at complete.org>
 #
@@ -17,12 +18,16 @@
 
 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())
diff --git a/lang/python/tests/Makefile.am b/lang/python/tests/Makefile.am
index 4a206fd..b2e725f 100644
--- a/lang/python/tests/Makefile.am
+++ b/lang/python/tests/Makefile.am
@@ -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
index 0000000..6e068a0
--- /dev/null
+++ b/lang/python/tests/encrypt-only.asc
@@ -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-----
diff --git a/lang/python/tests/initial.py b/lang/python/tests/initial.py
index 9d72cbc..169c3df 100755
--- a/lang/python/tests/initial.py
+++ b/lang/python/tests/initial.py
@@ -19,6 +19,20 @@
 
 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
index 0000000..6e2a6f3
--- /dev/null
+++ b/lang/python/tests/sign-only.asc
@@ -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-----
diff --git a/lang/python/tests/support.py b/lang/python/tests/support.py
index 8bafea8..f42fc2e 100644
--- a/lang/python/tests/support.py
+++ b/lang/python/tests/support.py
@@ -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)
diff --git a/lang/python/tests/t-decrypt-verify.py b/lang/python/tests/t-decrypt-verify.py
index 433e0a1..0f615dc 100755
--- a/lang/python/tests/t-decrypt-verify.py
+++ b/lang/python/tests/t-decrypt-verify.py
@@ -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"
diff --git a/lang/python/tests/t-decrypt.py b/lang/python/tests/t-decrypt.py
index bd7b59f..b5c4700 100755
--- a/lang/python/tests/t-decrypt.py
+++ b/lang/python/tests/t-decrypt.py
@@ -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'
diff --git a/lang/python/tests/t-encrypt-sign.py b/lang/python/tests/t-encrypt-sign.py
index cba697c..31cc94f 100755
--- a/lang/python/tests/t-encrypt-sign.py
+++ b/lang/python/tests/t-encrypt-sign.py
@@ -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"
diff --git a/lang/python/tests/t-encrypt-sym.py b/lang/python/tests/t-encrypt-sym.py
index 0b24fd5..c5be183 100755
--- a/lang/python/tests/t-encrypt-sym.py
+++ b/lang/python/tests/t-encrypt-sym.py
@@ -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"
diff --git a/lang/python/tests/t-encrypt.py b/lang/python/tests/t-encrypt.py
index 24869fc..4c77f39 100755
--- a/lang/python/tests/t-encrypt.py
+++ b/lang/python/tests/t-encrypt.py
@@ -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"
diff --git a/lang/python/tests/t-idiomatic.py b/lang/python/tests/t-idiomatic.py
index b252690..1989c92 100755
--- a/lang/python/tests/t-idiomatic.py
+++ b/lang/python/tests/t-idiomatic.py
@@ -20,13 +20,13 @@
 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)
diff --git a/lang/python/tests/t-keylist.py b/lang/python/tests/t-keylist.py
index ee9c283..64fec27 100755
--- a/lang/python/tests/t-keylist.py
+++ b/lang/python/tests/t-keylist.py
@@ -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], \
diff --git a/lang/python/tests/t-sign.py b/lang/python/tests/t-sign.py
index a721f03..802a32d 100755
--- a/lang/python/tests/t-sign.py
+++ b/lang/python/tests/t-sign.py
@@ -19,34 +19,38 @@
 
 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"
diff --git a/lang/python/tests/t-signers.py b/lang/python/tests/t-signers.py
index 26dded5..15e8011 100755
--- a/lang/python/tests/t-signers.py
+++ b/lang/python/tests/t-signers.py
@@ -18,35 +18,39 @@
 # 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'
diff --git a/lang/python/tests/t-verify.py b/lang/python/tests/t-verify.py
index 333ee4e..b88bd07 100755
--- a/lang/python/tests/t-verify.py
+++ b/lang/python/tests/t-verify.py
@@ -18,12 +18,13 @@
 # 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"

-----------------------------------------------------------------------

Summary of changes:
 configure.ac                          |   2 +-
 lang/python/pyme/__init__.py          |  69 ++------
 lang/python/pyme/core.py              | 324 +++++++++++++++++++++++++++++++---
 lang/python/pyme/errors.py            |  59 ++++++-
 lang/python/pyme/util.py              |  23 ++-
 lang/python/pyme/version.py.in        |   7 +-
 lang/python/tests/Makefile.am         |   2 +-
 lang/python/tests/encrypt-only.asc    |  33 ++++
 lang/python/tests/initial.py          |  14 ++
 lang/python/tests/sign-only.asc       |  33 ++++
 lang/python/tests/support.py          |  38 +++-
 lang/python/tests/t-decrypt-verify.py |  28 ++-
 lang/python/tests/t-decrypt.py        |   8 +
 lang/python/tests/t-encrypt-sign.py   |  24 +++
 lang/python/tests/t-encrypt-sym.py    |  20 +++
 lang/python/tests/t-encrypt.py        |  27 ++-
 lang/python/tests/t-idiomatic.py      |  17 +-
 lang/python/tests/t-keylist.py        |  14 +-
 lang/python/tests/t-sign.py           |  50 +++++-
 lang/python/tests/t-signers.py        |  37 +++-
 lang/python/tests/t-verify.py         |  84 +++++++--
 21 files changed, 777 insertions(+), 136 deletions(-)
 create mode 100644 lang/python/tests/encrypt-only.asc
 create mode 100644 lang/python/tests/sign-only.asc


hooks/post-receive
-- 
GnuPG Made Easy
http://git.gnupg.org




More information about the Gnupg-commits mailing list