python: Support quick subkey creation.

* NEWS: Update.
* lang/python/gpg/core.py (Context.create_subkey): New function.
* lang/python/tests/Makefile.am (XTESTS): Add new test.
* lang/python/tests/t-quick-subkey-creation.py: New file.

Signed-off-by: Justus Winter <justus@g10code.com>
This commit is contained in:
Justus Winter 2017-02-16 16:38:21 +01:00
parent 476b97822b
commit 13bace25e3
No known key found for this signature in database
GPG Key ID: DD1A52F9DA8C9020
4 changed files with 196 additions and 1 deletions

1
NEWS
View File

@ -22,6 +22,7 @@ Noteworthy changes in version 1.8.1 (unreleased)
py: Context.home_dir NEW.
py: Context.keylist EXTENDED: New keyword arg mode.
py: Context.create_key NEW.
py: Context.create_subkey NEW.
py: core.pubkey_algo_string NEW.
py: core.addrspec_from_uid NEW.

View File

@ -579,6 +579,78 @@ class Context(GpgmeWrapper):
return self.op_genkey_result()
def create_subkey(self, key, algorithm=None, expires_in=0, expires=True,
sign=False, encrypt=False, authenticate=False, passphrase=None):
"""Create a subkey
Create a subkey for the given KEY. As subkeys are a concept
of OpenPGP, calling this is only valid for the OpenPGP
protocol.
ALGORITHM may be used to specify the public key encryption
algorithm for the new subkey. By default, a reasonable
default is chosen. You may use "future-default" to select an
algorithm that will be the default in a future implementation
of the engine. ALGORITHM may be a string like "rsa", or
"rsa2048" to explicitly request an algorithm and a key size.
EXPIRES_IN specifies the expiration time of the subkey in
number of seconds since the subkeys creation. By default, a
reasonable expiration time is chosen. If you want to create a
subkey that does not expire, use the keyword argument EXPIRES.
SIGN, ENCRYPT, and AUTHENTICATE can be used to request the
capabilities of the new subkey. If you don't request any, an
encryption subkey is generated.
If PASSPHRASE is None (the default), then the subkey will not
be protected with a passphrase. If PASSPHRASE is a string, it
will be used to protect the subkey. If PASSPHRASE is True,
the passphrase must be supplied using a passphrase callback or
out-of-band with a pinentry.
Keyword arguments:
algorithm -- public key algorithm, see above (default: reasonable)
expires_in -- expiration time in seconds (default: reasonable)
expires -- whether or not the subkey should expire (default: True)
sign -- request the signing capability (see above)
encrypt -- request the encryption capability (see above)
authenticate -- request the authentication capability (see above)
passphrase -- protect the subkey with a passphrase (default: no passphrase)
Returns:
-- an object describing the result of the subkey creation
Raises:
GPGMEError -- as signaled by the underlying library
"""
if util.is_a_string(passphrase):
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:
self.op_createsubkey(key, algorithm,
0, # reserved
expires_in,
((constants.create.SIGN if sign else 0)
| (constants.create.ENCR if encrypt else 0)
| (constants.create.AUTH if authenticate else 0)
| (constants.create.NOPASSWD
if passphrase == None else 0)
| (0 if expires else constants.create.NOEXPIRE)))
finally:
if util.is_a_string(passphrase):
self.pinentry_mode = old_pinentry_mode
if old_passphrase_cb:
self.set_passphrase_cb(*old_passphrase_cb[1:])
return self.op_genkey_result()
def assuan_transact(self, command,
data_cb=None, inquire_cb=None, status_cb=None):
"""Issue a raw assuan command

View File

@ -51,7 +51,8 @@ py_tests = t-wrapper.py \
t-file-name.py \
t-idiomatic.py \
t-protocol-assuan.py \
t-quick-key-creation.py
t-quick-key-creation.py \
t-quick-subkey-creation.py
XTESTS = initial.py $(py_tests) final.py
EXTRA_DIST = support.py $(XTESTS) encrypt-only.asc sign-only.asc \

View File

@ -0,0 +1,121 @@
#!/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 os
import shutil
import time
import support
alpha = "Alpha <alpha@invalid.example.net>"
bravo = "Bravo <bravo@invalid.example.net>"
def copy_configuration(destination):
home = os.environ['GNUPGHOME']
shutil.copy(os.path.join(home, "gpg.conf"), destination)
shutil.copy(os.path.join(home, "gpg-agent.conf"), destination)
with support.TemporaryDirectory() as tmp:
copy_configuration(tmp)
with gpg.Context(home_dir=tmp) as ctx:
res = ctx.create_key(alpha, certify=True)
keys = list(ctx.keylist())
assert len(keys) == 1, "Weird number of keys created"
key = keys[0]
assert key.fpr == res.fpr
assert len(key.subkeys) == 1, "Expected one primary key and no subkeys"
def get_subkey(fpr):
k = ctx.get_key(fpr)
for sk in k.subkeys:
if sk.fpr == fpr:
return sk
return None
# Check gpg.constants.create.NOEXPIRE...
res = ctx.create_subkey(key, expires=False)
subkey = get_subkey(res.fpr)
assert subkey.expires == 0, "Expected subkey not to expire"
assert subkey.can_encrypt, \
"Default subkey capabilities do not include encryption"
t = 2 * 24 * 60 * 60
slack = 5 * 60
res = ctx.create_subkey(key, expires_in=t)
subkey = get_subkey(res.fpr)
assert abs(time.time() + t - subkey.expires) < slack, \
"subkeys expiration time is off"
# Check capabilities
for sign, encrypt, authenticate in itertools.product([False, True],
[False, True],
[False, True]):
# Filter some out
if not (sign or encrypt or authenticate):
# This triggers the default capabilities tested before.
continue
res = ctx.create_subkey(key, sign=sign, encrypt=encrypt,
authenticate=authenticate)
subkey = get_subkey(res.fpr)
assert sign == subkey.can_sign
assert encrypt == subkey.can_encrypt
assert authenticate == subkey.can_authenticate
# Check algorithm
res = ctx.create_subkey(key, algorithm="rsa")
subkey = get_subkey(res.fpr)
assert subkey.pubkey_algo == 1
# Check algorithm with size
res = ctx.create_subkey(key, algorithm="rsa1024")
subkey = get_subkey(res.fpr)
assert subkey.pubkey_algo == 1
assert subkey.length == 1024
# Check algorithm future-default
ctx.create_subkey(key, algorithm="future-default")
# Check passphrase protection. For this we create a new key
# so that we have a key with just one encryption subkey.
bravo_res = ctx.create_key(bravo, certify=True)
bravo_key = ctx.get_key(bravo_res.fpr)
assert len(bravo_key.subkeys) == 1, "Expected one primary key and no subkeys"
passphrase = "streng geheim"
res = ctx.create_subkey(bravo_key, passphrase=passphrase)
ciphertext, _, _ = ctx.encrypt(b"hello there",
recipients=[ctx.get_key(bravo_res.fpr)])
cb_called = False
def cb(*args):
global cb_called
cb_called = True
return passphrase
ctx.pinentry_mode = gpg.constants.PINENTRY_MODE_LOOPBACK
ctx.set_passphrase_cb(cb)
plaintext, _, _ = ctx.decrypt(ciphertext)
assert plaintext == b"hello there"
assert cb_called