diff --git a/README.rst b/README.rst index 498c36a..a64d784 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,16 @@ This respository offers the following functionality: which reflect to the sender what the bot perceived in terms of autocrypt information. +**Requirements** + +You need to separately install "gpg" or "gpg2" if you +want to use or manage keys with the system keyring. + **NOTE** -This implementation is not Level 1 compliant. -See #17. +This implementation is not Level 1 compliant. See #17. + +Also note there is a separate python autocrypt implementation +effort ongoing at https://github.com/juga0/pyac which is based +on the "pgpy" library and does not depend on the "gpg" command +line tool. We'd like to integrate with pgpy/pyac at a later stage. diff --git a/core/autocrypt/pgpycrypto.py b/core/autocrypt/pgpycrypto.py deleted file mode 100644 index b108854..0000000 --- a/core/autocrypt/pgpycrypto.py +++ /dev/null @@ -1,378 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab -"""PGPyCrypto implements the OpenPGP operations needed for Autocrypt. -The API is the same as in bingpg.py. -""" - -from __future__ import print_function, unicode_literals - -import glob -import os -import re -import sys - -import six -from pgpy import PGPUID, PGPKey, PGPMessage, PGPKeyring, PGPSignature -from pgpy.constants import (CompressionAlgorithm, HashAlgorithm, KeyFlags, - PubKeyAlgorithm, SymmetricKeyAlgorithm) - -# TODO: these two functions should be in a separate file -from .bingpg import KeyInfo, b64encode_u - - -# NOTE: key size was decided to be 2048 -KEY_SIZE = 2048 -# TODO: see which defaults we would like here -SKEY_ARGS = { - 'hashes': [HashAlgorithm.SHA512, HashAlgorithm.SHA256], - 'ciphers': [SymmetricKeyAlgorithm.AES256, - SymmetricKeyAlgorithm.AES192, - SymmetricKeyAlgorithm.AES128], - 'compression': [CompressionAlgorithm.ZLIB, - CompressionAlgorithm.BZ2, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.Uncompressed] -} -# RSAEncrypt is deprecated, therefore using RSAEncryptOrSign -# also for the subkey -SKEY_ALG = PubKeyAlgorithm.RSAEncryptOrSign -SKEY_USAGE_SIGN = {KeyFlags.Sign} -SKEY_USAGE_ENC = {KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage} -SKEY_USAGE_ALL = {KeyFlags.Sign, KeyFlags.EncryptCommunications, - KeyFlags.EncryptStorage} - - -def key_bytes(pgpykey): - """Key bytes. - - :param key: key (either public or private) - :type key: PGPKey - :return: key bytes - :rtype: string - - """ - assert isinstance(pgpykey, PGPKey) - if sys.version_info >= (3, 0): - keybytes = bytes(pgpykey) - else: - keybytes = pgpykey.__bytes__() - return keybytes - - -class PGPyCrypto(object): - """OpenPGP operations for Autocrypt using PGPy. - - PGPy does not currently support file system keyring, therefore - keys must be imported/exported from/to files or GPG keyring. - - .. todo:: - * instead of storing the keys in files, they could be stored - in the autocrypt config.json file. - * it probably would be better to create an abstract class - as interface from which both BinGPG and PGPyCrypto inherit - and then implement specific methods for BinGPG and PGPyCrypto. - - .. note:: - * methods not shared with BinGPG API are named as private. - * some operations are kept for compatibility with BinGPG API, - but they do not really make sense with PGPy. - """ - - def __init__(self, homedir=None, gpgpath="gpg"): - """Init PGPyCrypto class. - - :param homedir: home dir - :type key: str - :param gpgpath: kept for compatibility with BinGPG API - :type gpgpath: str - - """ - # NOTE: called as .._pgpy.. to know that is instance of PGPKey - self.pgpydir = homedir - self.memkr = PGPKeyring() - self._ensure_init() - - def __str__(self): - return "PGPyCrypto(homedir={homedir!r})".format( - homedir=self.pgpydir) - - def _ensure_init(self): - if self.pgpydir is None: - return - if not os.path.exists(self.pgpydir): - os.mkdir(self.pgpydir) - os.chmod(self.pgpydir, 0o700) - self._loadkr() - - def _loadkr(self): - keyfiles = glob.glob(os.path.join(self.pgpydir, '*.asc')) - self.memkr.load(keyfiles) - - def _savekr(self): - # NOTE: saving secret keys in clear in the filesystem is a secuirty - # risk, but the generated keys with GPG are not passphrase protected - # and PGPY does not implement yet filesystem key ring - for fp in self.memkr.fingerprints(): - with self.memkr.key(fp) as key: - self._save_key_to_file(key) - - def _key_path(self, key): - ext = '' - if key.is_public is False: - ext = '.sec' - ext += '.asc' - keypath = os.path.join(self.pgpydir, key.fingerprint.keyid + ext) - return keypath - - def _load_key_into_kr(self, key): - keypath = self._save_key_to_file(key) - self.memkr.load(keypath) - - def _save_key_to_file(self, key): - keypath = self._key_path(key) - with open(keypath, 'wb') as fd: - fd.write(key_bytes(key)) - return keypath - - def _gen_skey_usage_all(self, emailadr): - skey = PGPKey.new(SKEY_ALG, KEY_SIZE) - # NOTE: pgpy implements separate attributes for name and e-mail - # address. Name is mandatory. - # Here e-mail address is used for the attribute name, - # so that the uid is 'e-mail adress'. - # If name attribute would be set to empty string - # and email to the e-mail address, the uid would be - # ' ', which we do not want. - uid = PGPUID.new(emailadr) - skey.add_uid(uid, usage=SKEY_USAGE_ALL, **SKEY_ARGS) - return skey - - def _gen_ssubkey(self): - # NOTE: the uid for the subkeys can be obtained with .parent, - # but, unlike keys generated with gpg, it's not printed when imported - # in gpg keyring and run --fingerprint. - # in case of adding uid to the subkey, it raises currently some - # exceptions depending on which are the arguments used, which are not - # clear from the documentation. - ssubkey = PGPKey.new(SKEY_ALG, KEY_SIZE) - return ssubkey - - def _gen_skey_with_subkey(self, emailadr): - # NOTE: skey should be generated with usage sign, but otherwise - # encryption does not work currently. - skey = self._gen_skey_usage_all(emailadr) - ssubkey = self._gen_ssubkey() - skey.add_subkey(ssubkey, usage=SKEY_USAGE_ENC) - return skey - - def gen_secret_key(self, emailadr): - """Generate PGPKey object. - - :param emailadr: e-mail address - :return: keyhandle - :type emailadr: string - :rtype: string - - """ - skey = self._gen_skey_with_subkey(emailadr) - self._load_key_into_kr(skey) - return skey.fingerprint.keyid - - def supports_eddsa(self): - # NOTE: PGPy does not currently support it - return False - - def _get_key_from_keyhandle(self, keyhandle): - # NOTE: this is a bit unefficient, there should be other way to obtain - # a key from PGPKeyring - for fp in self.memkr.fingerprints(): - with self.memkr.key(fp) as key: - if key.fingerprint.keyid == keyhandle: - return key - return None - - def _key_data(self, key, armor=False, b64=False): - assert isinstance(key, PGPKey) - if armor is True: - keydata = str(key) - else: - keydata = key_bytes(key) - return keydata if not b64 else b64encode_u(keydata) - - def get_public_keydata(self, keyhandle=None, armor=False, b64=False): - key = self._get_key_from_keyhandle(keyhandle) - if key is not None: - pkey = key if key.is_public is True else key.pubkey - return self._key_data(pkey, armor, b64) - - def get_secret_keydata(self, keyhandle=None, armor=False): - key = self._get_key_from_keyhandle(keyhandle) - if key is not None: - skey = key if key.is_public is False else None - return self._key_data(skey, armor) if skey is not None else None - - def list_secret_keyinfos(self, keyhandle=None): - args = [keyhandle] if keyhandle is not None else [] - return self._parse_list(args, ("sec", "ssb")) - - def list_public_keyinfos(self, keyhandle=None): - args = [keyhandle] if keyhandle is not None else [] - return self._parse_list(args, ("pub", "sub")) - - def _parse_list(self, args, types): - # NOTE: the subkeys have to be at the end of the list to pass - # the tests - keyhandle = args[0] if args else None - keyinfos = [] - # NOTE: public keys with private key are not loaded in memkr, as the - # public part is in the private, so they have to be obtained from the - # from the private ones - keyhalf = 'public' if "pub" in types else 'private' - primaryfps = self.memkr.fingerprints(keytype='primary') - subfps = self.memkr.fingerprints(keytype='sub') - for fp in primaryfps: - with self.memkr.key(fp) as k: - if keyhalf is 'public': - k = k.pubkey - if keyhandle is None or keyhandle == k.fingerprint.keyid: - uid = k.userids[0].name - keyinfos.append(KeyInfo(type=k.key_algorithm.value, - bits=k.key_size, - uid=uid, - id=k.fingerprint.keyid, - date_created=k.created)) - for fp in subfps: - with self.memkr.key(fp) as k: - if keyhandle is None or \ - keyhandle == k.parent.fingerprint.keyid: - uid = k.parent.userids[0].name - if keyhalf is 'public': - k = k.pubkey - keyinfos.append(KeyInfo(type=k.key_algorithm.value, - bits=k.key_size, - uid=uid, - id=k.fingerprint.keyid, - date_created=k.created)) - return keyinfos - - def list_packets(self, keydata): - # NOTE: while is known how to get the packets from PGPKey, - # use gpg only here - import subprocess - key, _ = PGPKey.from_blob(keydata) - keypath = self._save_key_to_file(key) - sp = subprocess.Popen(['/usr/bin/gpg', '--list-packets', keypath], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = sp.communicate() - if sys.version_info >= (3, 0): - out = out.decode() - packets = [] - lines = [] - last_package_type = None - for rawline in out.splitlines(): - line = rawline.strip() - c = line[0:1] - if c == "#": - continue - if c == ":": - i = line[1:].find(c) - if i != -1: - ptype = line[1: i + 1] - pvalue = line[i + 2:].strip() - if last_package_type is not None: - packets.append(last_package_type + (lines,)) - lines = [] - last_package_type = (ptype, pvalue) - else: - assert last_package_type, line - lines.append(line) - else: - packets.append(last_package_type + (lines,)) - return packets - - def _find_keyhandle(self, string, - _pattern=re.compile("key (?:ID )?([0-9A-F]+)")): - # search for string like "key " - m = _pattern.search(string) - assert m and len(m.groups()) == 1, string - x = m.groups()[0] - # now search the fingerprint if we only have a shortid - if len(x) <= 8: - keyinfos = self.list_public_keyinfos(x) - for k in keyinfos: - if k.match(x): - return k.id - raise ValueError("could not find fingerprint %r in %r" % - (x, keyinfos)) - # note that this might be a 16-char fingerprint or a 40-char one - # (gpg-2.1.18) - return x - - def _encrypt_msg_with_pkey(self, data, key): - clear_msg = PGPMessage.new(data) - pkey = key if key.is_public else key.pubkey - enc_msg = pkey.encrypt(clear_msg) - return enc_msg - - def encrypt(self, data, recipients): - assert len(recipients) >= 1 - clear_msg = PGPMessage.new(data) - # enc_msg |= self.pgpykey.sign(enc_msg) - if len(recipients) == 1: - key = self._get_key_from_keyhandle(recipients[0]) - enc_msg = key.pubkey.encrypt(clear_msg) - else: - # The symmetric cipher should be specified, in case the first - # preferred cipher is not the same for all recipients public - # keys. - cipher = SymmetricKeyAlgorithm.AES256 - sessionkey = cipher.gen_key() - enc_msg = clear_msg - for r in recipients: - key = self._get_key_from_keyhandle(r) - enc_msg = key.pubkey.encrypt(enc_msg, cipher=cipher, - sessionkey=sessionkey) - del sessionkey - return str(enc_msg) - - def sign(self, data, keyhandle): - key = self._get_key_from_keyhandle(keyhandle) - sig_data = key.sign(data) - return sig_data - - def _skeys(self): - skeys = [] - secfps = self.memkr(keyhalf="private") - for fp in secfps: - with self.memkr.key(fp) as key: - skeys.append(key) - return skeys - - def verify(self, data, signature): - sig = PGPSignature(signature) \ - if isinstance(signature, str) else signature - keyhandle = sig.signer - key = self._get_key_from_keyhandle(keyhandle) - skey = key if key.is_public is False else key.pubkey - ver = skey.verify(data, signature) - good = next(ver.good_signatures) - return good.by - - def decrypt(self, enc_data): - if isinstance(enc_data, str): - enc_msg = PGPMessage.from_blob(enc_data) - else: - enc_msg = enc_data - keyhandle = enc_msg.issuers.pop() - skey = self._get_key_from_keyhandle(keyhandle) - out = skey.decrypt(enc_msg) - keyinfos = [] - keyinfos.append(KeyInfo(skey.key_algorithm.name, skey.key_size, - skey.fingerprint.keyid, skey.userids[0].name, - skey.created)) - return six.b(out.message), keyinfos - - def import_keydata(self, keydata): - key, _ = PGPKey.from_blob(keydata) - self._load_key_into_kr(key) - return key.fingerprint.keyid diff --git a/core/experiments/gpg_utils.py b/core/experiments/gpg_utils.py deleted file mode 100644 index 4114d21..0000000 --- a/core/experiments/gpg_utils.py +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab 2 - -"""Functions to create, file import/export OpenPGP keys""" -# FIXME: this file should be moved to ../autocrypt/ and possibly -# merged with gpg.py - -import logging -import sys -from base64 import b64encode -from pgpy import PGPKey, PGPUID -from pgpy.constants import PubKeyAlgorithm, KeyFlags, HashAlgorithm -from pgpy.constants import SymmetricKeyAlgorithm, CompressionAlgorithm - -logger = logging.getLogger(__name__) - - -def generate_rsa_key(uid='alice@testsuite.autocrypt.org', - alg_key=PubKeyAlgorithm.RSAEncryptOrSign, - alg_subkey=PubKeyAlgorithm.RSAEncryptOrSign, - size=2048): - # RSAEncrypt is deprecated, therefore using RSAEncryptOrSign - # also for the subkey - """Generate PGPKey object. - - :param alg_key: algorithm for primary key - :param alg_subkey: algorithm for subkey - :param size: key size - :param uid: e-mail address - :return: key - :type alg_key: PubKeyAlgorithm - :type alg_subkey: PubKeyAlgorithm - :type size: integer - :type uid: string - :rtype: PGPKey - - """ - # NOTE: default algorithm was decided to be RSA and size 2048. - key = PGPKey.new(alg_key, size) - # NOTE: pgpy implements separate attributes for name and e-mail address - # is mandatory. - # Here using e-mail address for the attribute name in order for - # the uid to be the e-mail address. If name attribute is set to - # empty string and email to the e-mail address, the uid will be ' - # ', for instance: - # " " - which we do not want. - uid = PGPUID.new(uid) - # NOTE: it is needed to specify all arguments in current pgpy version. - # FIXME: see which defaults we would like here - key.add_uid(uid, - usage={KeyFlags.Sign}, - hashes=[HashAlgorithm.SHA512, HashAlgorithm.SHA256], - ciphers=[SymmetricKeyAlgorithm.AES256, - SymmetricKeyAlgorithm.AES192, - SymmetricKeyAlgorithm.AES128], - compression=[CompressionAlgorithm.ZLIB, - CompressionAlgorithm.BZ2, - CompressionAlgorithm.ZIP, - CompressionAlgorithm.Uncompressed]) - subkey = PGPKey.new(alg_subkey, size) - key.add_subkey(subkey, usage={KeyFlags.EncryptCommunications, - KeyFlags.EncryptStorage}) - logger.debug('Created key with fingerprint %s', key.fingerprint) - return key - - -def generate_ec_key(): - # NOTE: pgpy does implement ed25519 nor cv25519 - pass - - -def key_from_file(key_path='/tmp/pubkey.asc'): - """Create PGPKey object from an armored key file (either public or private) - - :param key_path: path to the key - :return: key - :type key_path: string - :rtype: PGPKey - - """ - key, _ = PGPKey.from_file(key_path) - logger.debug('Imported key with fingerprint %s', key.fingerprint) - return key - - -def import_key_into_keyring(key, gnupghome_path='/tmp/gnupg'): - """Import key into a filesystem compatible GNUpg keyring - - :param key: key to import - :param gnupghome_path: keyring path - :type key: PGPKey - :type gnupghome_path: string - - .. note:: - pgpy does not implement filesystem keyring - """ - # NOTE: pgpy does not implement filesystem keyring - pass - - -def export_key_to_file(key, key_path='/tmp/key.asc'): - """Export key to file. - - :param key: key (either public or private) - :type key: PGPKey - :param key_path: filesystem path to write the key to - :type key_path: string - - """ - with open(key_path, 'w') as fp: - fp.write(str(key)) - logger.debug('Exported private key with fingerprint %s to file %s', key.fingerprint, key_path) - - -def export_pubkey_to_file(key, pubkey_path='/tmp/pubkey.asc'): - """Export public key to file from either a public or private key. - - :param key: key (either public or private) - :type key: PGPKey - :param key_path: filesystem path to write the key to - :type key_path: string - - """ - if key.is_public: - export_key_to_file(key, pubkey_path) - else: - pubkey = key.pubkey - export_key_to_file(pubkey, pubkey_path) - logger.debug('Exported public key with fingerprint %s to file %s', key.fingerprint, pubkey_path) - - -def key_fp(key): - """Key fingerprint. - - :param key: key (either public or private) - :type key: PGPKey - :return: key fingerprint - :rtype: string - - """ - return key.fingerprint - - -def key_armor_str(key): - """Key armored string. - - :param key: key (either public or private) - :type key: PGPKey - :return: key armored string - :rtype: string - - """ - return str(key) - - -def key_bytes(key): - """Key bytes. - - :param key: key (either public or private) - :type key: PGPKey - :return: key bytes - :rtype: string - - """ - if sys.version_info >= (3, 0): - keybytes = bytes(key) - else: - keybytes = key.__bytes__() - return keybytes - - -def key_base64(key): - """Base 64 representation of key bytes. - - :param key: key (either public or private) - :type key: PGPKey - :return: Base 64 representation of key bytes - :rtype: string - - """ - keybytes = key_bytes(key) - keybase64 = b64encode(keybytes) - return keybase64 diff --git a/core/tests/conftest.py b/core/tests/conftest.py index 99745ef..15e4c15 100644 --- a/core/tests/conftest.py +++ b/core/tests/conftest.py @@ -93,22 +93,6 @@ def maker(native=False): return maker -@pytest.fixture -def crypto_maker(request, tmpdir): - """Return a function which creates initialized PGPyCrypto instances.""" - counter = itertools.count() - - def maker(native=False): - from autocrypt.pgpycrypto import PGPyCrypto - if native: - pgpycrypto = PGPyCrypto() - else: - p = tmpdir.join("pgpycrypto%d" % next(counter)) - pgpycrypto = PGPyCrypto(p.strpath) - return pgpycrypto - return maker - - @pytest.fixture def bingpg(bingpg_maker): """ return an initialized bingpg instance. """ @@ -121,12 +105,6 @@ def bingpg2(bingpg_maker): return bingpg_maker() -@pytest.fixture -def pgpycrypto(crypto_maker): - """Return an initialized pgpycrypto instance.""" - return crypto_maker() - - class ClickRunner: def __init__(self, main): self.runner = CliRunner() diff --git a/core/tests/test_pgpycrypto.py b/core/tests/test_pgpycrypto.py deleted file mode 100644 index a92d030..0000000 --- a/core/tests/test_pgpycrypto.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab -"""Tests for PGPyCrypto""" - -from __future__ import unicode_literals - -import pytest - - -class TestCrypto: - def test_gen_key_and_get_keydata(self, pgpycrypto): - keyhandle = pgpycrypto.gen_secret_key(emailadr="hello@xyz.org") - skey_data = pgpycrypto.get_secret_keydata(keyhandle, armor=True) - packets = pgpycrypto.list_packets(skey_data) - assert len(packets) == 5 - assert packets[0][0] == "secret key packet" - assert packets[1][0] == "user ID packet" - # NOTE: the correct string here is not " " - assert packets[1][1] == '"hello@xyz.org"' - assert packets[2][0] == "signature packet" - assert packets[3][0] == "secret sub key packet" - assert packets[4][0] == "signature packet" - - pkey_data = pgpycrypto.get_public_keydata(keyhandle) - packets = pgpycrypto.list_packets(pkey_data) - assert len(packets) == 5 - assert packets[0][0] == "public key packet" == packets[0][0] - assert packets[1][0] == "user ID packet" - # NOTE: the correct string here is not " " - assert packets[1][1] == '"hello@xyz.org"' - assert packets[2][0] == "signature packet" - assert packets[3][0] == "public sub key packet" - assert packets[4][0] == "signature packet" - - def test_list_secret_keyhandles(self, pgpycrypto): - keyhandle = pgpycrypto.gen_secret_key(emailadr="hello@xyz.org") - l = pgpycrypto.list_secret_keyinfos(keyhandle) - assert len(l) == 2 - assert l[0].id == keyhandle - - def test_list_public_keyhandles(self, pgpycrypto): - keyhandle = pgpycrypto.gen_secret_key(emailadr="hello@xyz.org") - l = pgpycrypto.list_public_keyinfos(keyhandle) - assert len(l) == 2 - assert l[0].match(keyhandle) - - @pytest.mark.parametrize("armor", [True, False]) - def test_transfer_key_and_encrypt_decrypt_roundtrip(self, pgpycrypto, - armor): - keyhandle = pgpycrypto.gen_secret_key(emailadr="hello@xyz.org") - # FIXME: nothing is done with priv_keydata in test_bingpg (nor here) - # priv_keydata = pgpycrypto.get_secret_keydata(keyhandle=keyhandle, - # armor=armor) - public_keydata = pgpycrypto.get_public_keydata(keyhandle=keyhandle, - armor=armor) - keyhandle2 = pgpycrypto.import_keydata(public_keydata) - assert keyhandle2 == keyhandle - - out_encrypt = pgpycrypto.encrypt(b"123", recipients=[keyhandle]) - out, decrypt_info = pgpycrypto.decrypt(out_encrypt) - assert out == b"123" - assert len(decrypt_info) == 1 - k = decrypt_info[0] - assert str(k) - assert k.bits == 2048 - assert k.type == "RSAEncryptOrSign" - assert k.date_created - keyinfos = pgpycrypto.list_public_keyinfos(keyhandle) - for keyinfo in keyinfos: - if keyinfo.match(k.id): - break - else: - pytest.fail("decryption key {!r} not found in {}". - format(k.id, keyinfos)) - - def test_gen_key_and_sign_verify(self, pgpycrypto): - keyhandle = pgpycrypto.gen_secret_key(emailadr="hello@xyz.org") - sig = pgpycrypto.sign(b"123", keyhandle=keyhandle) - keyhandle_verified = pgpycrypto.verify(data=b'123', signature=sig) - i = min(len(keyhandle_verified), len(keyhandle)) - assert keyhandle[-i:] == keyhandle_verified[-i:] diff --git a/core/tox.ini b/core/tox.ini index 3a7acdb..4131bce 100644 --- a/core/tox.ini +++ b/core/tox.ini @@ -5,9 +5,7 @@ skip_missing_interpreters = True [testenv] deps = pytest - pytest-catchlog pytest-localserver - pgpy>=0.4.1 commands = pytest {posargs:--no-test-cache --with-gpg2} @@ -15,7 +13,6 @@ commands = [testenv:doc] deps = sphinx - pgpy>=0.4.1 whitelist_externals = make changedir = ../doc commands = diff --git a/doc/api.rst b/doc/api.rst index f7dce7e..152ca16 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -16,7 +16,6 @@ Autocrypt Python API Reference autocrypt.bot autocrypt.mime autocrypt.bingpg - autocrypt.pgpycrypto account module -------------- @@ -42,15 +41,6 @@ bingpg module .. automodule:: autocrypt.bingpg :members: -pgpycrypto module ------------------- - -.. note:: - - The "pgpy" backend is tested but not used not used yet because - pgpy==0.4.1 are not sufficiently substituting gpg functionality yet. - -.. automodule:: autocrypt.pgpycrypto claimchain module -----------------