python: Support quick key signing.
authorJustus Winter <justus@g10code.com>
Fri, 17 Feb 2017 14:44:35 +0000 (15:44 +0100)
committerJustus Winter <justus@g10code.com>
Fri, 17 Feb 2017 15:28:00 +0000 (16:28 +0100)
* NEWS: Update.
* doc/gpgme.texi (gpgme_op_keysign): Fix the description of the
'expire' argument.
* lang/python/gpg/constants/__init__.py: Import new file.
* lang/python/gpg/constants/keysign.py: New file.
* lang/python/gpg/core.py (Context.key_sign): New function.
* lang/python/tests/Makefile.am (py_tests): Add new test.
* lang/python/tests/t-quick-key-signing.py: New test.

Signed-off-by: Justus Winter <justus@g10code.com>
NEWS
doc/gpgme.texi
lang/python/gpg/constants/__init__.py
lang/python/gpg/constants/keysign.py [new file with mode: 0644]
lang/python/gpg/core.py
lang/python/tests/Makefile.am
lang/python/tests/t-quick-key-signing.py [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 889a526..617d1d3 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,7 @@ Noteworthy changes in version 1.8.1 (unreleased)
  py: Context.create_subkey   NEW.
  py: Context.key_add_uid     NEW.
  py: Context.key_revoke_uid  NEW.
+ py: Context.key_sign        NEW.
  py: core.pubkey_algo_string NEW.
  py: core.addrspec_from_uid  NEW.
 
index c088cfe..78225d5 100644 (file)
@@ -4044,11 +4044,10 @@ object (@code{gpgme_user_id_t}) is to be used.  To select more than
 one user ID put them all into one string separated by linefeeds
 characters (@code{\n}) and set the flag @code{GPGME_KEYSIGN_LFSEP}.
 
-@var{expires} can be set to the number of seconds since Epoch of the
-desired expiration date in UTC for the new signature.  The common case
-is to use 0 to not set an expiration date.  However, if the
-configuration of the engine defines a default expiration for key
-signatures, that is still used unless the flag
+@var{expires} specifies the expiration time of the new signature in
+seconds.  The common case is to use 0 to not set an expiration date.
+However, if the configuration of the engine defines a default
+expiration for key signatures, that is still used unless the flag
 @code{GPGME_KEYSIGN_NOEXPIRE} is used.  Note that this parameter takes
 an unsigned long value and not a @code{time_t} to avoid problems on
 systems which use a signed 32 bit @code{time_t}.  Note further that
index 2bf180e..79d1fbc 100644 (file)
@@ -26,14 +26,14 @@ del util
 
 # For convenience, we import the modules here.
 from . import data, keylist, sig # The subdirs.
-from . import create, event, md, pk, protocol, sigsum, status, validity
+from . import create, event, keysign, md, pk, protocol, sigsum, status, validity
 
 # A complication arises because 'import' is a reserved keyword.
 # Import it as 'Import' instead.
 globals()['Import'] = getattr(__import__('', globals(), locals(),
                                          [str('import')], 1), "import")
 
-__all__ = ['data', 'event', 'import', 'keylist', 'md', 'pk',
+__all__ = ['data', 'event', 'import', 'keysign', 'keylist', 'md', 'pk',
            'protocol', 'sig', 'sigsum', 'status', 'validity', 'create']
 
 # GPGME 1.7 replaced gpgme_op_edit with gpgme_op_interact.  We
diff --git a/lang/python/gpg/constants/keysign.py b/lang/python/gpg/constants/keysign.py
new file mode 100644 (file)
index 0000000..fccdbc4
--- /dev/null
@@ -0,0 +1,25 @@
+# Flags for key signing
+#
+# Copyright (C) 2017 g10 Code GmbH
+#
+# This file is part of GPGME.
+#
+# GPGME is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+#
+# GPGME is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
+#
+# 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/>.
+
+from __future__ import absolute_import, print_function, unicode_literals
+del absolute_import, print_function, unicode_literals
+
+from gpg import util
+util.process_constants('GPGME_KEYSIGN_', globals())
+del util
index 28d4629..cb4ccf7 100644 (file)
@@ -675,6 +675,47 @@ class Context(GpgmeWrapper):
         """
         self.op_revuid(key, uid, 0)
 
+    def key_sign(self, key, uids=None, expires_in=False, local=False):
+        """Sign a key
+
+        Sign a key with the current set of signing keys.  Calling this
+        function is only valid for the OpenPGP protocol.
+
+        If UIDS is None (the default), then all UIDs are signed.  If
+        it is a string, then only the matching UID is signed.  If it
+        is a list of strings, then all matching UIDs are signed.  Note
+        that a case-sensitive exact string comparison is done.
+
+        EXPIRES_IN specifies the expiration time of the signature in
+        seconds.  If EXPIRES_IN is False, the signature does not
+        expire.
+
+        Keyword arguments:
+        uids         -- user ids to sign, see above (default: sign all)
+        expires_in   -- validity period of the signature in seconds
+                                               (default: do not expire)
+        local        -- create a local, non-exportable signature
+                                               (default: False)
+
+        Raises:
+        GPGMEError   -- as signaled by the underlying library
+
+        """
+        flags = 0
+        if uids == None or util.is_a_string(uids):
+            pass#through unchanged
+        else:
+            flags |= constants.keysign.LFSEP
+            uids = "\n".join(uids)
+
+        if not expires_in:
+            flags |= constants.keysign.NOEXPIRE
+
+        if local:
+            flags |= constants.keysign.LOCAL
+
+        self.op_keysign(key, uids, expires_in, flags)
+
     def assuan_transact(self, command,
                         data_cb=None, inquire_cb=None, status_cb=None):
         """Issue a raw assuan command
index 1d5e1db..7251cd3 100644 (file)
@@ -53,7 +53,8 @@ py_tests = t-wrapper.py \
        t-protocol-assuan.py \
        t-quick-key-creation.py \
        t-quick-subkey-creation.py \
-       t-quick-key-manipulation.py
+       t-quick-key-manipulation.py \
+       t-quick-key-signing.py
 
 XTESTS = initial.py $(py_tests) final.py
 EXTRA_DIST = support.py $(XTESTS) encrypt-only.asc sign-only.asc \
diff --git a/lang/python/tests/t-quick-key-signing.py b/lang/python/tests/t-quick-key-signing.py
new file mode 100755 (executable)
index 0000000..f9778a3
--- /dev/null
@@ -0,0 +1,120 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2017 g10 Code GmbH
+#
+# This file is part of GPGME.
+#
+# GPGME is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# GPGME is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
+#
+# 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/>.
+
+from __future__ import absolute_import, print_function, unicode_literals
+del absolute_import, print_function, unicode_literals
+
+import gpg
+import itertools
+import time
+
+import support
+
+with support.EphemeralContext() as ctx:
+    uid_counter = 0
+    def make_uid():
+        global uid_counter
+        uid_counter += 1
+        return "user{0}@invalid.example.org".format(uid_counter)
+
+    def make_key():
+        uids = [make_uid() for i in range(3)]
+        res = ctx.create_key(uids[0], certify=True)
+        key = ctx.get_key(res.fpr)
+        for u in uids[1:]:
+            ctx.key_add_uid(key, u)
+        return key, uids
+
+    def check_sigs(key, expected_sigs):
+        keys = list(ctx.keylist(key.fpr, mode=(gpg.constants.keylist.mode.LOCAL
+                                               |gpg.constants.keylist.mode.SIGS)))
+        assert len(keys) == 1
+        key_uids = {uid.uid: [s for s in uid.signatures] for uid in keys[0].uids}
+        expected = list(expected_sigs)
+
+        while key_uids and expected:
+            uid, signing_key, func = expected[0]
+            match = False
+            for i, s in enumerate(key_uids[uid]):
+                if signing_key.fpr.endswith(s.keyid):
+                    if func:
+                        func(s)
+                    match = True
+                    break
+            if match:
+                expected.pop(0)
+                key_uids[uid].pop(i)
+                if not key_uids[uid]:
+                    del key_uids[uid]
+
+        assert not key_uids, "Superfluous signatures: {0}".format(key_uids)
+        assert not expected, "Missing signatures: {0}".format(expected)
+
+    # Simplest case.  Sign without any options.
+    key_a, uids_a = make_key()
+    key_b, uids_b = make_key()
+    ctx.signers = [key_a]
+
+    def exportable_non_expiring(s):
+        assert s.exportable
+        assert s.expires == 0
+
+    check_sigs(key_b, itertools.product(uids_b, [key_b], [exportable_non_expiring]))
+    ctx.key_sign(key_b)
+    check_sigs(key_b, itertools.product(uids_b, [key_b, key_a], [exportable_non_expiring]))
+
+    # Create a non-exportable signature, and explicitly name all uids.
+    key_c, uids_c = make_key()
+    ctx.signers = [key_a, key_b]
+
+    def non_exportable_non_expiring(s):
+        assert s.exportable == 0
+        assert s.expires == 0
+
+    ctx.key_sign(key_c, local=True, uids=uids_c)
+    check_sigs(key_c,
+               list(itertools.product(uids_c, [key_c],
+                                      [exportable_non_expiring]))
+               + list(itertools.product(uids_c, [key_b, key_a],
+                                        [non_exportable_non_expiring])))
+
+    # Create a non-exportable, expiring signature for a single uid.
+    key_d, uids_d = make_key()
+    ctx.signers = [key_c]
+    expires_in = 600
+    slack = 10
+
+    def non_exportable_expiring(s):
+        assert s.exportable == 0
+        assert abs(time.time() + expires_in - s.expires) < slack
+
+    ctx.key_sign(key_d, local=True, expires_in=expires_in, uids=uids_d[0])
+    check_sigs(key_d,
+               list(itertools.product(uids_d, [key_d],
+                                      [exportable_non_expiring]))
+               + list(itertools.product(uids_d[:1], [key_c],
+                                        [non_exportable_expiring])))
+
+    # Now sign the second in the same fashion, but use a singleton list.
+    ctx.key_sign(key_d, local=True, expires_in=expires_in, uids=uids_d[1:2])
+    check_sigs(key_d,
+               list(itertools.product(uids_d, [key_d],
+                                      [exportable_non_expiring]))
+               + list(itertools.product(uids_d[:2], [key_c],
+                                        [non_exportable_expiring])))