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
             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
         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.
 
 
  * 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
 ----------------
 
 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.
 
 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
 --------------------
 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']
 
 """
 
 __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 re
+import os
 import weakref
 from . import pygpgme
 from .errors import errorcheck, GPGMEError
 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"""
     @property
     def signers(self):
         """Keys used for signing"""
@@ -204,32 +502,6 @@ class Context(GpgmeWrapper):
         return 0
 
     _boolean_properties = {'armor', 'textmode', 'offline'}
         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:
 
     def __del__(self):
         if not pygpgme:
index f96877b..0194931 100644 (file)
@@ -20,7 +20,10 @@ from . import util
 
 util.process_constants('GPG_ERR_', globals())
 
 
 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
     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 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)
 
 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>
 #
 # Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net>
 # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org>
 #
 
 from . import pygpgme
 
 
 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
        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~ \
 
 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 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"])
 
 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
 
 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 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:
 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/>.
 
 # 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
 
 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 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)
     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()
 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)
                     "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/>.
 
 # 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
 
 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)
     "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
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import pyme
 from pyme import core, constants
 import support
 
 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)
     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
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import os
+import pyme
 from pyme import core, constants
 import support
 
 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)
     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/>.
 
 # 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
 
 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, \
 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)
 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
 import io
 import os
 import tempfile
-from pyme import core, constants, errors
+import pyme
 import support
 
 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:
 
 # 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
     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):
 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]
         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"
 
     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.
 
 # 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)
     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)
         "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)
         "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])
 
     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"
 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], \
       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 sys
 import os
+import pyme
 from pyme import core, constants
 import support
 
 from pyme import core, constants
 import support
 
+def fail(msg):
+    raise RuntimeError(msg)
+
 def check_result(r, typ):
     if r.invalid_signers:
 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:
 
     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:
 
     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:
 
     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:
             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:
             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":
             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)
 
 
 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)
 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
 # License along with this program; if not, see <http://www.gnu.org/licenses/>.
 
 import sys
+import pyme
 from pyme import core, constants
 import support
 
 from pyme import core, constants
 import support
 
+def fail(msg):
+    raise RuntimeError(msg)
+
 def check_result(r, typ):
     if r.invalid_signers:
 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:
 
     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:
 
     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:
 
         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:
                 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:
                 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"):
 
         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)
 
 
 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)
     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
 # 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
 
 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
 
 iN0EABECAJ0FAjoS+i9FFIAAAAAAAwA5YmFyw7bDpMO8w58gZGFzIHdhcmVuIFVt
 bGF1dGUgdW5kIGpldHp0IGVpbiBwcm96ZW50JS1aZWljaGVuNRSAAAAAAAgAJGZv
@@ -34,7 +35,7 @@ dADGKXF/Hcb+AKCJWPphZCphduxSvrzH0hgzHdeQaA==
 -----END PGP SIGNATURE-----
 """
 
 -----END PGP SIGNATURE-----
 """
 
-test_sig2 = """-----BEGIN PGP MESSAGE-----
+test_sig2 = b"""-----BEGIN PGP MESSAGE-----
 
 owGbwMvMwCSoW1RzPCOz3IRxjXQSR0lqcYleSUWJTZOvjVdpcYmCu1+oQmaJIleH
 GwuDIBMDGysTSIqBi1MApi+nlGGuwDeHao53HBr+FoVGP3xX+kvuu9fCMJvl6IOf
 
 owGbwMvMwCSoW1RzPCOz3IRxjXQSR0lqcYleSUWJTZOvjVdpcYmCu1+oQmaJIleH
 GwuDIBMDGysTSIqBi1MApi+nlGGuwDeHao53HBr+FoVGP3xX+kvuu9fCMJvl6IOf
@@ -44,7 +45,7 @@ y1kvP4y+8D5a11ang0udywsA
 """
 
 # A message with a prepended but unsigned plaintext packet.
 """
 
 # A message with a prepended but unsigned plaintext packet.
-double_plaintext_sig = """-----BEGIN PGP MESSAGE-----
+double_plaintext_sig = b"""-----BEGIN PGP MESSAGE-----
 
 rDRiCmZvb2Jhci50eHRF4pxNVGhpcyBpcyBteSBzbmVha3kgcGxhaW50ZXh0IG1l
 c3NhZ2UKowGbwMvMwCSoW1RzPCOz3IRxTWISa6JebnG666MFD1wzSzJSixQ81XMV
 
 rDRiCmZvb2Jhci50eHRF4pxNVGhpcyBpcyBteSBzbmVha3kgcGxhaW50ZXh0IG1l
 c3NhZ2UKowGbwMvMwCSoW1RzPCOz3IRxTWISa6JebnG666MFD1wzSzJSixQ81XMV
@@ -55,10 +56,12 @@ UqVooWlGXHwNw/xg/fVzt9VNbtjtJ/fhUqYo0/LyCGEA
 -----END PGP MESSAGE-----
 """
 
 -----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 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
 
     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 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
 
 
     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()
 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)
 
 
              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()
 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()
 
 # 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.
              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."
     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"