diff --git a/NEWS b/NEWS
index 889a5265..617d1d37 100644
--- 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.
diff --git a/doc/gpgme.texi b/doc/gpgme.texi
index c088cfed..78225d58 100644
--- a/doc/gpgme.texi
+++ b/doc/gpgme.texi
@@ -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
diff --git a/lang/python/gpg/constants/__init__.py b/lang/python/gpg/constants/__init__.py
index 2bf180e5..79d1fbc1 100644
--- a/lang/python/gpg/constants/__init__.py
+++ b/lang/python/gpg/constants/__init__.py
@@ -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
index 00000000..fccdbc42
--- /dev/null
+++ b/lang/python/gpg/constants/keysign.py
@@ -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 .
+
+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
diff --git a/lang/python/gpg/core.py b/lang/python/gpg/core.py
index 28d4629e..cb4ccf73 100644
--- a/lang/python/gpg/core.py
+++ b/lang/python/gpg/core.py
@@ -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
diff --git a/lang/python/tests/Makefile.am b/lang/python/tests/Makefile.am
index 1d5e1db5..7251cd30 100644
--- a/lang/python/tests/Makefile.am
+++ b/lang/python/tests/Makefile.am
@@ -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
index 00000000..f9778a33
--- /dev/null
+++ b/lang/python/tests/t-quick-key-signing.py
@@ -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 .
+
+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])))