python: Support quick key signing.

* 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>
This commit is contained in:
Justus Winter 2017-02-17 15:44:35 +01:00
parent de8494b16b
commit 48634e651f
No known key found for this signature in database
GPG Key ID: DD1A52F9DA8C9020
7 changed files with 195 additions and 8 deletions

1
NEWS
View File

@ -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.

View 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

View 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

View File

@ -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

View 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

View 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 \

View File

@ -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])))