diff options
| -rw-r--r-- | NEWS | 1 | ||||
| -rw-r--r-- | lang/python/gpg/core.py | 72 | ||||
| -rw-r--r-- | lang/python/tests/Makefile.am | 3 | ||||
| -rwxr-xr-x | lang/python/tests/t-quick-subkey-creation.py | 121 | 
4 files changed, 196 insertions, 1 deletions
| @@ -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. diff --git a/lang/python/gpg/core.py b/lang/python/gpg/core.py index c5af1b18..2a4df99b 100644 --- a/lang/python/gpg/core.py +++ b/lang/python/gpg/core.py @@ -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 diff --git a/lang/python/tests/Makefile.am b/lang/python/tests/Makefile.am index 5469e751..62c6087f 100644 --- a/lang/python/tests/Makefile.am +++ b/lang/python/tests/Makefile.am @@ -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 \ diff --git a/lang/python/tests/t-quick-subkey-creation.py b/lang/python/tests/t-quick-subkey-creation.py new file mode 100755 index 00000000..0d9f71fb --- /dev/null +++ b/lang/python/tests/t-quick-subkey-creation.py @@ -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 <[email protected]>" +bravo = "Bravo <[email protected]>" + +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 | 
