summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-10-06 16:01:25 -0400
committerKali Kaneko <kali@leap.se>2015-10-06 16:01:25 -0400
commitf404e4abc426ce08850283345ce162fb9b5403cf (patch)
tree1cef68094f840e960db784762aa8c10c60b5e15b
parenta4be602e5882a566b5b484deaa97f544602a309c (diff)
parent9a9c53eea49092e80737c84a2f850dd682c33ae3 (diff)
Merge branch 'develop' into debian/experimental
pre-release for 0.4.3
-rw-r--r--README.rst22
-rw-r--r--changes/bug-7410_fetch_key1
-rw-r--r--changes/bug-7498_multiple_keys1
-rw-r--r--changes/bug-address_mixup1
-rw-r--r--src/leap/keymanager/__init__.py147
-rw-r--r--src/leap/keymanager/openpgp.py273
-rw-r--r--src/leap/keymanager/tests/__init__.py14
-rw-r--r--src/leap/keymanager/tests/test_keymanager.py126
-rw-r--r--src/leap/keymanager/tests/test_openpgp.py104
-rw-r--r--src/leap/keymanager/tests/test_validation.py26
10 files changed, 546 insertions, 169 deletions
diff --git a/README.rst b/README.rst
index acf2335..1d4b53e 100644
--- a/README.rst
+++ b/README.rst
@@ -1,11 +1,16 @@
LEAP's Key Manager
==================
-.. image:: https://pypip.in/v/leap.keymanager/badge.png
- :target: https://crate.io/packages/leap.keymanager
+.. image:: https://badge.fury.io/py/leap.keymanager.svg
+ :target: http://badge.fury.io/py/leap.keymanager
+.. image:: https://img.shields.io/pypi/dm/leap.keymanager.svg
+ :target: http://badge.fury.io/py/leap.keymanager
-The Key Manager is a Nicknym agent for the LEAP project:
+The Key Manager is a `Nicknym`_ agent for the `LEAP`_ project, written in python using the `twisted`_ async framework.
+
+.. _`Nicknym`: https://leap.se/nicknym
+.. _`LEAP`: https://leap.se/docs/
+.. _`twisted`: https://twistedmatrix.com/trac/
- https://leap.se/pt/docs/design/nicknym
running tests
-------------
@@ -13,3 +18,12 @@ running tests
Use trial to run the test suite::
trial leap.keymanager
+
+License
+=======
+
+.. image:: https://raw.github.com/leapcode/bitmask_client/develop/docs/user/gpl.png
+
+leap.keymanager is released under the terms of the `GNU GPL version 3`_ or later.
+
+.. _`GNU GPL version 3`: http://www.gnu.org/licenses/gpl.txt
diff --git a/changes/bug-7410_fetch_key b/changes/bug-7410_fetch_key
new file mode 100644
index 0000000..4aec9fe
--- /dev/null
+++ b/changes/bug-7410_fetch_key
@@ -0,0 +1 @@
+- catch request exceptions on key fetching
diff --git a/changes/bug-7498_multiple_keys b/changes/bug-7498_multiple_keys
new file mode 100644
index 0000000..90cf675
--- /dev/null
+++ b/changes/bug-7498_multiple_keys
@@ -0,0 +1 @@
+- self-repair the keyring if keys get duplicated (Closes: #7485)
diff --git a/changes/bug-address_mixup b/changes/bug-address_mixup
new file mode 100644
index 0000000..24170c9
--- /dev/null
+++ b/changes/bug-address_mixup
@@ -0,0 +1 @@
+- Don't repush a public key with different address
diff --git a/src/leap/keymanager/__init__.py b/src/leap/keymanager/__init__.py
index 999b53c..c7886e0 100644
--- a/src/leap/keymanager/__init__.py
+++ b/src/leap/keymanager/__init__.py
@@ -18,7 +18,13 @@
Key Manager is a Nicknym agent for LEAP client.
"""
# let's do a little sanity check to see if we're using the wrong gnupg
+import fileinput
+import os
import sys
+import tempfile
+
+from leap.common import ca_bundle
+
from ._version import get_versions
try:
@@ -55,7 +61,7 @@ from twisted.internet import defer
from urlparse import urlparse
from leap.common.check import leap_assert
-from leap.common.events import emit, catalog
+from leap.common.events import emit_async, catalog
from leap.common.decorators import memoized_method
from leap.keymanager.errors import (
@@ -74,8 +80,6 @@ from leap.keymanager.keys import (
)
from leap.keymanager.openpgp import OpenPGPKey, OpenPGPScheme
-from ._version import get_versions
-
__version__ = get_versions()['version']
del get_versions
@@ -136,12 +140,43 @@ class KeyManager(object):
}
# the following are used to perform https requests
self._fetcher = requests
- self._session = self._fetcher.session()
+ self._combined_ca_bundle = self._create_combined_bundle_file()
+
+ #
+ # destructor
+ #
+
+ def __del__(self):
+ try:
+ created_tmp_combined_ca_bundle = self._combined_ca_bundle not in \
+ [ca_bundle.where(), self._ca_cert_path]
+ if created_tmp_combined_ca_bundle:
+ os.remove(self._combined_ca_bundle)
+ except OSError:
+ pass
#
# utilities
#
+ def _create_combined_bundle_file(self):
+ leap_ca_bundle = ca_bundle.where()
+
+ if self._ca_cert_path == leap_ca_bundle:
+ return self._ca_cert_path # don't merge file with itself
+ elif not self._ca_cert_path:
+ return leap_ca_bundle
+
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+
+ with open(tmp_file.name, 'w') as fout:
+ fin = fileinput.input(files=(leap_ca_bundle, self._ca_cert_path))
+ for line in fin:
+ fout.write(line)
+ fin.close()
+
+ return tmp_file.name
+
def _key_class_from_type(self, ktype):
"""
Return key class from string representation of key type.
@@ -178,6 +213,24 @@ class KeyManager(object):
# 'Content-type is not JSON.')
return res
+ def _get_with_combined_ca_bundle(self, uri, data=None):
+ """
+ Send a GET request to C{uri} containing C{data}.
+
+ Instead of using the ca_cert provided on construction time, this
+ version also uses the default certificates shipped with leap.common
+
+ :param uri: The URI of the request.
+ :type uri: str
+ :param data: The body of the request.
+ :type data: dict, str or file
+
+ :return: The response to the request.
+ :rtype: requests.Response
+ """
+ return self._fetcher.get(
+ uri, data=data, verify=self._combined_ca_bundle)
+
def _put(self, uri, data=None):
"""
Send a PUT request to C{uri} containing C{data}.
@@ -287,7 +340,7 @@ class KeyManager(object):
self._api_version,
self._uid)
self._put(uri, data)
- emit(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, self._address)
+ emit_async(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, self._address)
d = self.get_key(
self._address, ktype, private=False, fetch_remote=False)
@@ -323,33 +376,34 @@ class KeyManager(object):
leap_assert(
ktype in self._wrapper_map,
'Unkown key type: %s.' % str(ktype))
- emit(catalog.KEYMANAGER_LOOKING_FOR_KEY, address)
+ _keys = self._wrapper_map[ktype]
+
+ emit_async(catalog.KEYMANAGER_LOOKING_FOR_KEY, address)
def key_found(key):
- emit(catalog.KEYMANAGER_KEY_FOUND, address)
+ emit_async(catalog.KEYMANAGER_KEY_FOUND, address)
return key
def key_not_found(failure):
if not failure.check(KeyNotFound):
return failure
- emit(catalog.KEYMANAGER_KEY_NOT_FOUND, address)
+ emit_async(catalog.KEYMANAGER_KEY_NOT_FOUND, address)
# we will only try to fetch a key from nickserver if fetch_remote
# is True and the key is not private.
if fetch_remote is False or private is True:
return failure
- emit(catalog.KEYMANAGER_LOOKING_FOR_KEY, address)
+ emit_async(catalog.KEYMANAGER_LOOKING_FOR_KEY, address)
d = self._fetch_keys_from_server(address)
d.addCallback(
- lambda _:
- self._wrapper_map[ktype].get_key(address, private=False))
+ lambda _: _keys.get_key(address, private=False))
d.addCallback(key_found)
return d
# return key if it exists in local database
- d = self._wrapper_map[ktype].get_key(address, private=private)
+ d = _keys.get_key(address, private=private)
d.addCallbacks(key_found, key_not_found)
return d
@@ -395,13 +449,16 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
def signal_finished(key):
- emit(catalog.KEYMANAGER_FINISHED_KEY_GENERATION, self._address)
+ emit_async(
+ catalog.KEYMANAGER_FINISHED_KEY_GENERATION, self._address)
return key
- emit(catalog.KEYMANAGER_STARTED_KEY_GENERATION, self._address)
- d = self._wrapper_map[ktype].gen_key(self._address)
+ emit_async(catalog.KEYMANAGER_STARTED_KEY_GENERATION, self._address)
+
+ d = _keys.gen_key(self._address)
d.addCallback(signal_finished)
return d
@@ -491,14 +548,15 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
def encrypt(keys):
pubkey, signkey = keys
- encrypted = self._wrapper_map[ktype].encrypt(
+ encrypted = _keys.encrypt(
data, pubkey, passphrase, sign=signkey,
cipher_algo=cipher_algo)
pubkey.encr_used = True
- d = self._wrapper_map[ktype].put_key(pubkey, address)
+ d = _keys.put_key(pubkey, address)
d.addCallback(lambda _: encrypted)
return d
@@ -543,18 +601,21 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
def decrypt(keys):
pubkey, privkey = keys
- decrypted, signed = self._wrapper_map[ktype].decrypt(
+ decrypted, signed = _keys.decrypt(
data, privkey, passphrase=passphrase, verify=pubkey)
if pubkey is None:
signature = KeyNotFound(verify)
elif signed:
- pubkey.sign_used = True
- d = self._wrapper_map[ktype].put_key(pubkey, address)
- d.addCallback(lambda _: (decrypted, pubkey))
- return d
+ signature = pubkey
+ if not pubkey.sign_used:
+ pubkey.sign_used = True
+ d = _keys.put_key(pubkey, verify)
+ d.addCallback(lambda _: (decrypted, signature))
+ return d
else:
signature = InvalidSignature(
'Failed to verify signature with key %s' %
@@ -603,9 +664,10 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
def sign(privkey):
- return self._wrapper_map[ktype].sign(
+ return _keys.sign(
data, privkey, digest_algo=digest_algo, clearsign=clearsign,
detach=detach, binary=binary)
@@ -641,15 +703,18 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
def verify(pubkey):
- signed = self._wrapper_map[ktype].verify(
+ signed = _keys.verify(
data, pubkey, detached_sig=detached_sig)
if signed:
- pubkey.sign_used = True
- d = self._wrapper_map[ktype].put_key(pubkey, address)
- d.addCallback(lambda _: pubkey)
- return d
+ if not pubkey.sign_used:
+ pubkey.sign_used = True
+ d = _keys.put_key(pubkey, address)
+ d.addCallback(lambda _: pubkey)
+ return d
+ return pubkey
else:
raise InvalidSignature(
'Failed to verify signature with key %s' %
@@ -674,7 +739,8 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(type(key))
- return self._wrapper_map[type(key)].delete_key(key)
+ _keys = self._wrapper_map[type(key)]
+ return _keys.delete_key(key)
def put_key(self, key, address):
"""
@@ -694,7 +760,9 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
- self._assert_supported_key_type(type(key))
+ ktype = type(key)
+ self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
if address not in key.address:
return defer.fail(
@@ -709,14 +777,13 @@ class KeyManager(object):
def check_upgrade(old_key):
if key.private or can_upgrade(key, old_key):
- return self._wrapper_map[type(key)].put_key(key, address)
+ return _keys.put_key(key, address)
else:
raise KeyNotValidUpgrade(
"Key %s can not be upgraded by new key %s"
% (old_key.key_id, key.key_id))
- d = self._wrapper_map[type(key)].get_key(address,
- private=key.private)
+ d = _keys.get_key(address, private=key.private)
d.addErrback(old_key_not_found)
d.addCallback(check_upgrade)
return d
@@ -746,7 +813,9 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
- pubkey, privkey = self._wrapper_map[ktype].parse_ascii_key(key)
+ _keys = self._wrapper_map[ktype]
+
+ pubkey, privkey = _keys.parse_ascii_key(key)
pubkey.validation = validation
d = self.put_key(pubkey, address)
if privkey is not None:
@@ -779,13 +848,19 @@ class KeyManager(object):
:raise UnsupportedKeyTypeError: if invalid key type
"""
self._assert_supported_key_type(ktype)
+ _keys = self._wrapper_map[ktype]
- res = self._get(uri)
+ logger.info("Fetch key for %s from %s" % (address, uri))
+ try:
+ res = self._get_with_combined_ca_bundle(uri)
+ except Exception as e:
+ logger.warning("There was a problem fetching key: %s" % (e,))
+ return defer.fail(KeyNotFound(uri))
if not res.ok:
return defer.fail(KeyNotFound(uri))
# XXX parse binary keys
- pubkey, _ = self._wrapper_map[ktype].parse_ascii_key(res.content)
+ pubkey, _ = _keys.parse_ascii_key(res.content)
if pubkey is None:
return defer.fail(KeyNotFound(uri))
diff --git a/src/leap/keymanager/openpgp.py b/src/leap/keymanager/openpgp.py
index 794a0ec..d648137 100644
--- a/src/leap/keymanager/openpgp.py
+++ b/src/leap/keymanager/openpgp.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# openpgp.py
-# Copyright (C) 2013 LEAP
+# Copyright (C) 2013-2015 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,6 +22,7 @@ import os
import re
import shutil
import tempfile
+import traceback
import io
@@ -41,6 +42,8 @@ from leap.keymanager.keys import (
TYPE_ADDRESS_PRIVATE_INDEX,
KEY_ADDRESS_KEY,
KEY_ID_KEY,
+ KEY_FINGERPRINT_KEY,
+ KEY_REFRESHED_AT_KEY,
KEYMANAGER_ACTIVE_TYPE,
)
@@ -251,7 +254,7 @@ class OpenPGPScheme(EncryptionScheme):
leap_assert(is_address(address), 'Not an user address: %s' % address)
def _gen_key(_):
- with self._temporary_gpgwrapper() as gpg:
+ with TempGPGWrapper(gpgbinary=self._gpgbinary) as gpg:
# TODO: inspect result, or use decorator
params = gpg.gen_key_input(
key_type='RSA',
@@ -321,7 +324,9 @@ class OpenPGPScheme(EncryptionScheme):
raise errors.KeyNotFound(address)
leap_assert(
address in doc.content[KEY_ADDRESS_KEY],
- 'Wrong address in key data.')
+ 'Wrong address in key %s. Expected %s, found %s.'
+ % (doc.content[KEY_ID_KEY], address,
+ doc.content[KEY_ADDRESS_KEY]))
key = build_key_from_dict(OpenPGPKey, doc.content)
key._gpgbinary = self._gpgbinary
return key
@@ -346,37 +351,25 @@ class OpenPGPScheme(EncryptionScheme):
# TODO: add more checks for correct key data.
leap_assert(key_data is not None, 'Data does not represent a key.')
- with self._temporary_gpgwrapper() as gpg:
- # TODO: inspect result, or use decorator
- gpg.import_keys(key_data)
- privkey = None
- pubkey = None
+ priv_info, privkey = process_ascii_key(
+ key_data, self._gpgbinary, secret=True)
+ pub_info, pubkey = process_ascii_key(
+ key_data, self._gpgbinary, secret=False)
- try:
- privkey = gpg.list_keys(secret=True).pop()
- except IndexError:
- pass
- try:
- pubkey = gpg.list_keys(secret=False).pop() # unitary keyring
- except IndexError:
- return (None, None)
-
- openpgp_privkey = None
- if privkey is not None:
- # build private key
- openpgp_privkey = self._build_key_from_gpg(
- privkey,
- gpg.export_keys(privkey['fingerprint'], secret=True))
- leap_check(pubkey['fingerprint'] == privkey['fingerprint'],
- 'Fingerprints for public and private key differ.',
- errors.KeyFingerprintMismatch)
-
- # build public key
- openpgp_pubkey = self._build_key_from_gpg(
- pubkey,
- gpg.export_keys(pubkey['fingerprint'], secret=False))
-
- return (openpgp_pubkey, openpgp_privkey)
+ if not pubkey:
+ return (None, None)
+
+ openpgp_privkey = None
+ if privkey:
+ # build private key
+ openpgp_privkey = self._build_key_from_gpg(priv_info, privkey)
+ leap_check(pub_info['fingerprint'] == priv_info['fingerprint'],
+ 'Fingerprints for public and private key differ.',
+ errors.KeyFingerprintMismatch)
+ # build public key
+ openpgp_pubkey = self._build_key_from_gpg(pub_info, pubkey)
+
+ return (openpgp_pubkey, openpgp_privkey)
def put_ascii_key(self, key_data, address):
"""
@@ -432,41 +425,42 @@ class OpenPGPScheme(EncryptionScheme):
:rtype: Deferred
"""
def check_and_put(docs, key):
- if len(docs) == 1:
- doc = docs.pop()
- oldkey = build_key_from_dict(OpenPGPKey, doc.content)
- if key.fingerprint == oldkey.fingerprint:
- # in case of an update of the key merge them with gnupg
- with self._temporary_gpgwrapper() as gpg:
- gpg.import_keys(oldkey.key_data)
- gpg.import_keys(key.key_data)
- gpgkey = gpg.list_keys(secret=key.private).pop()
- mergedkey = self._build_key_from_gpg(
- gpgkey,
- gpg.export_keys(gpgkey['fingerprint'],
- secret=key.private))
- mergedkey.validation = max(
- [key.validation, oldkey.validation])
- mergedkey.last_audited_at = oldkey.last_audited_at
- mergedkey.refreshed_at = key.refreshed_at
- mergedkey.encr_used = key.encr_used or oldkey.encr_used
- mergedkey.sign_used = key.sign_used or oldkey.sign_used
- doc.set_json(mergedkey.get_json())
- d = self._soledad.put_doc(doc)
- else:
- logger.critical(
- "Can't put a key whith the same key_id and different "
- "fingerprint: %s, %s"
- % (key.fingerprint, oldkey.fingerprint))
- d = defer.fail(
- errors.KeyFingerprintMismatch(key.fingerprint))
+ deferred_repair = defer.succeed(None)
+ if len(docs) == 0:
+ return self._soledad.create_doc_from_json(key.get_json())
elif len(docs) > 1:
+ deferred_repair = self._repair_key_docs(docs, key.key_id)
+
+ doc = docs[0]
+ oldkey = build_key_from_dict(OpenPGPKey, doc.content)
+ if key.fingerprint != oldkey.fingerprint:
logger.critical(
- "There is more than one key with the same key_id %s"
- % (key.key_id,))
- d = defer.fail(errors.KeyAttributesDiffer(key.key_id))
- else:
- d = self._soledad.create_doc_from_json(key.get_json())
+ "Can't put a key whith the same key_id and different "
+ "fingerprint: %s, %s"
+ % (key.fingerprint, oldkey.fingerprint))
+ return defer.fail(
+ errors.KeyFingerprintMismatch(key.fingerprint))
+
+ # in case of an update of the key merge them with gnupg
+ with TempGPGWrapper(gpgbinary=self._gpgbinary) as gpg:
+ gpg.import_keys(oldkey.key_data)
+ gpg.import_keys(key.key_data)
+ gpgkey = gpg.list_keys(secret=key.private).pop()
+ mergedkey = self._build_key_from_gpg(
+ gpgkey,
+ gpg.export_keys(gpgkey['fingerprint'],
+ secret=key.private))
+ mergedkey.validation = max(
+ [key.validation, oldkey.validation])
+ mergedkey.last_audited_at = oldkey.last_audited_at
+ mergedkey.refreshed_at = key.refreshed_at
+ mergedkey.encr_used = key.encr_used or oldkey.encr_used
+ mergedkey.sign_used = key.sign_used or oldkey.sign_used
+ doc.set_json(mergedkey.get_json())
+ deferred_put = self._soledad.put_doc(doc)
+
+ d = defer.gatherResults([deferred_put, deferred_repair])
+ d.addCallback(lambda res: res[0])
return d
d = self._soledad.get_from_index(
@@ -543,14 +537,21 @@ class OpenPGPScheme(EncryptionScheme):
self.KEY_TYPE,
key_id,
'1' if private else '0')
- d.addCallback(get_doc, key_id)
+ d.addCallback(get_doc, key_id, activedoc)
return d
- def get_doc(doclist, key_id):
- leap_assert(
- len(doclist) is 1,
- 'There is %d keys for id %s!' % (len(doclist), key_id))
- return doclist.pop()
+ def get_doc(doclist, key_id, activedoc):
+ if len(doclist) == 0:
+ logger.warning('There is no key for id %s! Self-repairing it.'
+ % (key_id))
+ d = self._soledad.delete_doc(activedoc)
+ d.addCallback(lambda _: None)
+ return d
+ elif len(doclist) > 1:
+ d = self._repair_key_docs(doclist, key_id)
+ d.addCallback(lambda _: doclist[0])
+ return d
+ return doclist[0]
d = self._soledad.get_from_index(
TYPE_ADDRESS_PRIVATE_INDEX,
@@ -575,24 +576,7 @@ class OpenPGPScheme(EncryptionScheme):
:return: An instance of the key.
:rtype: OpenPGPKey
"""
- expiry_date = None
- if key['expires']:
- expiry_date = datetime.fromtimestamp(int(key['expires']))
- address = []
- for uid in key['uids']:
- address.append(_parse_address(uid))
-
- return OpenPGPKey(
- address,
- gpgbinary=self._gpgbinary,
- key_id=key['keyid'],
- fingerprint=key['fingerprint'],
- key_data=key_data,
- private=True if key['type'] == 'sec' else False,
- length=int(key['length']),
- expiry_date=expiry_date,
- refreshed_at=datetime.now(),
- )
+ return build_gpg_key(key, key_data, self._gpgbinary)
def delete_key(self, key):
"""
@@ -625,18 +609,20 @@ class OpenPGPScheme(EncryptionScheme):
def delete_key(docs):
if len(docs) == 0:
raise errors.KeyNotFound(key)
- if len(docs) > 1:
- logger.critical("There is more than one key for key_id %s"
- % key.key_id)
-
- doc = None
- for d in docs:
- if d.content['fingerprint'] == key.fingerprint:
- doc = d
- break
- if doc is None:
+ elif len(docs) > 1:
+ logger.warning("There is more than one key for key_id %s"
+ % key.key_id)
+
+ has_deleted = False
+ deferreds = []
+ for doc in docs:
+ if doc.content['fingerprint'] == key.fingerprint:
+ d = self._soledad.delete_doc(doc)
+ deferreds.append(d)
+ has_deleted = True
+ if not has_deleted:
raise errors.KeyNotFound(key)
- return self._soledad.delete_doc(doc)
+ return defer.gatherResults(deferreds)
d = self._soledad.get_from_index(
TYPE_ID_PRIVATE_INDEX,
@@ -648,24 +634,39 @@ class OpenPGPScheme(EncryptionScheme):
d.addCallback(delete_key)
return d
- #
- # Data encryption, decryption, signing and verifying
- #
+ def _repair_key_docs(self, doclist, key_id):
+ """
+ If there is more than one key for a key id try to self-repair it
- def _temporary_gpgwrapper(self, keys=None):
+ :return: a Deferred that will be fired once all the deletions are
+ completed
+ :rtype: Deferred
"""
- Return a gpg wrapper that implements the context manager protocol and
- contains C{keys}.
+ logger.error("BUG ---------------------------------------------------")
+ logger.error("There is more than one key with the same key_id %s:"
+ % (key_id,))
- :param keys: keys to conform the keyring.
- :type key: list(OpenPGPKey)
+ def log_key_doc(doc):
+ logger.error("\t%s: %s" % (doc.content[KEY_ADDRESS_KEY],
+ doc.content[KEY_FINGERPRINT_KEY]))
- :return: a TempGPGWrapper instance
- :rtype: TempGPGWrapper
- """
- # TODO do here checks on key_data
- return TempGPGWrapper(
- keys=keys, gpgbinary=self._gpgbinary)
+ doclist.sort(key=lambda doc: doc.content[KEY_REFRESHED_AT_KEY],
+ reverse=True)
+ log_key_doc(doclist[0])
+ deferreds = []
+ for doc in doclist[1:]:
+ log_key_doc(doc)
+ d = self._soledad.delete_doc(doc)
+ deferreds.append(d)
+
+ logger.error("")
+ logger.error(traceback.extract_stack())
+ logger.error("BUG (please report above info) ------------------------")
+ return defer.gatherResults(deferreds, consumeErrors=True)
+
+ #
+ # Data encryption, decryption, signing and verifying
+ #
@staticmethod
def _assert_gpg_result_ok(result):
@@ -711,7 +712,7 @@ class OpenPGPScheme(EncryptionScheme):
leap_assert_type(sign, OpenPGPKey)
leap_assert(sign.private is True)
keys.append(sign)
- with self._temporary_gpgwrapper(keys) as gpg:
+ with TempGPGWrapper(keys, self._gpgbinary) as gpg:
result = gpg.encrypt(
data, pubkey.fingerprint,
default_key=sign.key_id if sign else None,
@@ -753,7 +754,7 @@ class OpenPGPScheme(EncryptionScheme):
leap_assert_type(verify, OpenPGPKey)
leap_assert(verify.private is False)
keys.append(verify)
- with self._temporary_gpgwrapper(keys) as gpg:
+ with TempGPGWrapper(keys, self._gpgbinary) as gpg:
try:
result = gpg.decrypt(
data, passphrase=passphrase, always_trust=True)
@@ -781,7 +782,7 @@ class OpenPGPScheme(EncryptionScheme):
:return: Whether C{data} was encrypted using this wrapper.
:rtype: bool
"""
- with self._temporary_gpgwrapper() as gpg:
+ with TempGPGWrapper(gpgbinary=self._gpgbinary) as gpg:
gpgutil = GPGUtilities(gpg)
return gpgutil.is_encrypted_asym(data)
@@ -812,7 +813,7 @@ class OpenPGPScheme(EncryptionScheme):
# result.fingerprint - contains the fingerprint of the key used to
# sign.
- with self._temporary_gpgwrapper(privkey) as gpg:
+ with TempGPGWrapper(privkey, self._gpgbinary) as gpg:
result = gpg.sign(data, default_key=privkey.key_id,
digest_algo=digest_algo, clearsign=clearsign,
detach=detach, binary=binary)
@@ -847,7 +848,7 @@ class OpenPGPScheme(EncryptionScheme):
"""
leap_assert_type(pubkey, OpenPGPKey)
leap_assert(pubkey.private is False)
- with self._temporary_gpgwrapper(pubkey) as gpg:
+ with TempGPGWrapper(pubkey, self._gpgbinary) as gpg:
result = None
if detached_sig is None:
result = gpg.verify(data)
@@ -865,3 +866,35 @@ class OpenPGPScheme(EncryptionScheme):
rfprint = result.fingerprint
kfprint = gpgpubkey['fingerprint']
return valid and rfprint == kfprint
+
+
+def process_ascii_key(key_data, gpgbinary, secret=False):
+ with TempGPGWrapper(gpgbinary=gpgbinary) as gpg:
+ try:
+ gpg.import_keys(key_data)
+ info = gpg.list_keys(secret=secret).pop()
+ key = gpg.export_keys(info['fingerprint'], secret=secret)
+ except IndexError:
+ info = {}
+ key = None
+ return info, key
+
+
+def build_gpg_key(key_info, key_data, gpgbinary=None):
+ expiry_date = None
+ if key_info['expires']:
+ expiry_date = datetime.fromtimestamp(int(key_info['expires']))
+ address = []
+ for uid in key_info['uids']:
+ address.append(_parse_address(uid))
+
+ return OpenPGPKey(
+ address,
+ gpgbinary=gpgbinary,
+ key_id=key_info['keyid'],
+ fingerprint=key_info['fingerprint'],
+ key_data=key_data,
+ private=True if key_info['type'] == 'sec' else False,
+ length=int(key_info['length']),
+ expiry_date=expiry_date,
+ refreshed_at=datetime.now())
diff --git a/src/leap/keymanager/tests/__init__.py b/src/leap/keymanager/tests/__init__.py
index 7128d20..cd612c4 100644
--- a/src/leap/keymanager/tests/__init__.py
+++ b/src/leap/keymanager/tests/__init__.py
@@ -66,18 +66,27 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest):
for private in [True, False]:
d = km.get_all_keys(private=private)
d.addCallback(delete_keys)
+ d.addCallback(check_deleted, private)
deferreds.append(d)
return gatherResults(deferreds)
+ def check_deleted(_, private):
+ d = km.get_all_keys(private=private)
+ d.addCallback(lambda keys: self.assertEqual(keys, []))
+ return d
+
# wait for the indexes to be ready for the tear down
d = km._wrapper_map[OpenPGPKey].deferred_indexes
d.addCallback(get_and_delete_keys)
d.addCallback(lambda _: self.tearDownEnv())
+ d.addCallback(lambda _: self._soledad.close())
return d
- def _key_manager(self, user=ADDRESS, url='', token=None):
+ def _key_manager(self, user=ADDRESS, url='', token=None,
+ ca_cert_path=None):
return KeyManager(user, url, self._soledad, token=token,
- gpgbinary=self.gpg_binary_path)
+ gpgbinary=self.gpg_binary_path,
+ ca_cert_path=ca_cert_path)
def _find_gpg(self):
gpg_path = distutils.spawn.find_executable('gpg')
@@ -88,6 +97,7 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest):
# key 24D18DDF: public key "Leap Test Key <leap@leap.se>"
+KEY_ID = "2F455E2824D18DDF"
KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF"
PUBLIC_KEY = """
-----BEGIN PGP PUBLIC KEY BLOCK-----
diff --git a/src/leap/keymanager/tests/test_keymanager.py b/src/leap/keymanager/tests/test_keymanager.py
index a12cac0..856d6da 100644
--- a/src/leap/keymanager/tests/test_keymanager.py
+++ b/src/leap/keymanager/tests/test_keymanager.py
@@ -20,9 +20,11 @@
Tests for the Key Manager.
"""
-
+from os import path
from datetime import datetime
-from mock import Mock
+import tempfile
+from leap.common import ca_bundle
+from mock import Mock, MagicMock, patch
from twisted.internet.defer import inlineCallbacks
from twisted.trial import unittest
@@ -50,6 +52,7 @@ from leap.keymanager.tests import (
NICKSERVER_URI = "http://leap.se/"
+REMOTE_KEY_URL = "http://site.domain/key"
class KeyManagerUtilTestCase(unittest.TestCase):
@@ -287,7 +290,6 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
content = PUBLIC_KEY
km._fetcher.get = Mock(return_value=Response())
- km.ca_cert_path = 'cacertpath'
yield km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey)
key = yield km.get_key(ADDRESS, OpenPGPKey)
@@ -304,7 +306,6 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
content = ""
km._fetcher.get = Mock(return_value=Response())
- km.ca_cert_path = 'cacertpath'
d = km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey)
return self.assertFailure(d, KeyNotFound)
@@ -320,10 +321,125 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
content = PUBLIC_KEY
km._fetcher.get = Mock(return_value=Response())
- km.ca_cert_path = 'cacertpath'
d = km.fetch_key(ADDRESS_2, "http://site.domain/key", OpenPGPKey)
return self.assertFailure(d, KeyAddressMismatch)
+ def _mock_get_response(self, km, body):
+ class Response(object):
+ ok = True
+ content = body
+
+ mock = MagicMock(return_value=Response())
+ km._fetcher.get = mock
+
+ return mock
+
+ @inlineCallbacks
+ def test_fetch_key_uses_ca_bundle_if_none_specified(self):
+ ca_cert_path = None
+ km = self._key_manager(ca_cert_path=ca_cert_path)
+ get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER)
+
+ yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL, OpenPGPKey)
+
+ get_mock.assert_called_once_with(REMOTE_KEY_URL, data=None,
+ verify=ca_bundle.where())
+
+ @inlineCallbacks
+ def test_fetch_key_uses_ca_bundle_if_empty_string_specified(self):
+ ca_cert_path = ''
+ km = self._key_manager(ca_cert_path=ca_cert_path)
+ get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER)
+
+ yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL, OpenPGPKey)
+
+ get_mock.assert_called_once_with(REMOTE_KEY_URL, data=None,
+ verify=ca_bundle.where())
+
+ @inlineCallbacks
+ def test_fetch_key_use_default_ca_bundle_if_set_as_ca_cert_path(self):
+ ca_cert_path = ca_bundle.where()
+ km = self._key_manager(ca_cert_path=ca_cert_path)
+ get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER)
+
+ yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL, OpenPGPKey)
+
+ get_mock.assert_called_once_with(REMOTE_KEY_URL, data=None,
+ verify=ca_bundle.where())
+
+ @inlineCallbacks
+ def test_fetch_uses_combined_ca_bundle_otherwise(self):
+ with tempfile.NamedTemporaryFile() as tmp_input, \
+ tempfile.NamedTemporaryFile(delete=False) as tmp_output:
+ ca_content = 'some\ncontent\n'
+ ca_cert_path = tmp_input.name
+ self._dump_to_file(ca_cert_path, ca_content)
+
+ with patch('leap.keymanager.tempfile.NamedTemporaryFile') as mock:
+ mock.return_value = tmp_output
+ km = self._key_manager(ca_cert_path=ca_cert_path)
+ get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER)
+
+ yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL, OpenPGPKey)
+
+ # assert that combined bundle file is passed to get call
+ get_mock.assert_called_once_with(REMOTE_KEY_URL, data=None,
+ verify=tmp_output.name)
+
+ # assert that files got appended
+ expected = self._slurp_file(ca_bundle.where()) + ca_content
+ self.assertEqual(expected, self._slurp_file(tmp_output.name))
+
+ del km # force km out of scope
+ self.assertFalse(path.exists(tmp_output.name))
+
+ def _dump_to_file(self, filename, content):
+ with open(filename, 'w') as out:
+ out.write(content)
+
+ def _slurp_file(self, filename):
+ with open(filename) as f:
+ content = f.read()
+ return content
+
+ @inlineCallbacks
+ def test_decrypt_updates_sign_used_for_signer(self):
+ # given
+ km = self._key_manager()
+ yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS)
+ yield km._wrapper_map[OpenPGPKey].put_ascii_key(
+ PRIVATE_KEY_2, ADDRESS_2)
+ encdata = yield km.encrypt('data', ADDRESS, OpenPGPKey,
+ sign=ADDRESS_2, fetch_remote=False)
+ yield km.decrypt(
+ encdata, ADDRESS, OpenPGPKey, verify=ADDRESS_2, fetch_remote=False)
+
+ # when
+ key = yield km.get_key(ADDRESS_2, OpenPGPKey, fetch_remote=False)
+
+ # then
+ self.assertEqual(True, key.sign_used)
+
+ @inlineCallbacks
+ def test_decrypt_does_not_update_sign_used_for_recipient(self):
+ # given
+ km = self._key_manager()
+ yield km._wrapper_map[OpenPGPKey].put_ascii_key(
+ PRIVATE_KEY, ADDRESS)
+ yield km._wrapper_map[OpenPGPKey].put_ascii_key(
+ PRIVATE_KEY_2, ADDRESS_2)
+ encdata = yield km.encrypt('data', ADDRESS, OpenPGPKey,
+ sign=ADDRESS_2, fetch_remote=False)
+ yield km.decrypt(
+ encdata, ADDRESS, OpenPGPKey, verify=ADDRESS_2, fetch_remote=False)
+
+ # when
+ key = yield km.get_key(
+ ADDRESS, OpenPGPKey, private=False, fetch_remote=False)
+
+ # then
+ self.assertEqual(False, key.sign_used)
+
class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase):
diff --git a/src/leap/keymanager/tests/test_openpgp.py b/src/leap/keymanager/tests/test_openpgp.py
index 5f85c74..bae83db 100644
--- a/src/leap/keymanager/tests/test_openpgp.py
+++ b/src/leap/keymanager/tests/test_openpgp.py
@@ -21,12 +21,15 @@ Tests for the OpenPGP support on Key Manager.
"""
-from twisted.internet.defer import inlineCallbacks
+from datetime import datetime
+from mock import Mock
+from twisted.internet.defer import inlineCallbacks, gatherResults, succeed
from leap.keymanager import (
KeyNotFound,
openpgp,
)
+from leap.keymanager.keys import TYPE_ID_PRIVATE_INDEX
from leap.keymanager.openpgp import OpenPGPKey
from leap.keymanager.tests import (
KeyManagerWithSoledadTestCase,
@@ -34,6 +37,7 @@ from leap.keymanager.tests import (
ADDRESS_2,
KEY_FINGERPRINT,
PUBLIC_KEY,
+ KEY_ID,
PUBLIC_KEY_2,
PRIVATE_KEY,
PRIVATE_KEY_2,
@@ -247,6 +251,104 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase):
validsign = pgp.verify(data, pubkey, detached_sig=signature)
self.assertTrue(validsign)
+ @inlineCallbacks
+ def test_self_repair_three_keys(self):
+ pgp = openpgp.OpenPGPScheme(
+ self._soledad, gpgbinary=self.gpg_binary_path)
+ yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS)
+
+ get_from_index = self._soledad.get_from_index
+ delete_doc = self._soledad.delete_doc
+
+ def my_get_from_index(*args):
+ if (args[0] == TYPE_ID_PRIVATE_INDEX and
+ args[2] == KEY_ID):
+ k1 = OpenPGPKey(ADDRESS, key_id="1",
+ refreshed_at=datetime(2005, 1, 1))
+ k2 = OpenPGPKey(ADDRESS, key_id="2",
+ refreshed_at=datetime(2007, 1, 1))
+ k3 = OpenPGPKey(ADDRESS, key_id="3",
+ refreshed_at=datetime(2001, 1, 1))
+ d1 = self._soledad.create_doc_from_json(k1.get_json())
+ d2 = self._soledad.create_doc_from_json(k2.get_json())
+ d3 = self._soledad.create_doc_from_json(k3.get_json())
+ return gatherResults([d1, d2, d3])
+ return get_from_index(*args)
+
+ self._soledad.get_from_index = my_get_from_index
+ self._soledad.delete_doc = Mock(return_value=succeed(None))
+
+ key = yield pgp.get_key(ADDRESS, private=False)
+
+ try:
+ self.assertEqual(key.key_id, "2")
+ self.assertEqual(self._soledad.delete_doc.call_count, 2)
+ finally:
+ self._soledad.get_from_index = get_from_index
+ self._soledad.delete_doc = delete_doc
+
+ @inlineCallbacks
+ def test_self_repair_no_keys(self):
+ pgp = openpgp.OpenPGPScheme(
+ self._soledad, gpgbinary=self.gpg_binary_path)
+ yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS)
+
+ get_from_index = self._soledad.get_from_index
+ delete_doc = self._soledad.delete_doc
+
+ def my_get_from_index(*args):
+ if (args[0] == TYPE_ID_PRIVATE_INDEX and
+ args[2] == KEY_ID):
+ return succeed([])
+ return get_from_index(*args)
+
+ self._soledad.get_from_index = my_get_from_index
+ self._soledad.delete_doc = Mock(return_value=succeed(None))
+
+ try:
+ yield self.assertFailure(pgp.get_key(ADDRESS, private=False),
+ KeyNotFound)
+ self.assertEqual(self._soledad.delete_doc.call_count, 1)
+ finally:
+ self._soledad.get_from_index = get_from_index
+ self._soledad.delete_doc = delete_doc
+
+ @inlineCallbacks
+ def test_self_repair_put_keys(self):
+ pgp = openpgp.OpenPGPScheme(
+ self._soledad, gpgbinary=self.gpg_binary_path)
+
+ get_from_index = self._soledad.get_from_index
+ delete_doc = self._soledad.delete_doc
+
+ def my_get_from_index(*args):
+ if (args[0] == TYPE_ID_PRIVATE_INDEX and
+ args[2] == KEY_ID):
+ k1 = OpenPGPKey(ADDRESS, key_id="1",
+ fingerprint=KEY_FINGERPRINT,
+ refreshed_at=datetime(2005, 1, 1))
+ k2 = OpenPGPKey(ADDRESS, key_id="2",
+ fingerprint=KEY_FINGERPRINT,
+ refreshed_at=datetime(2007, 1, 1))
+ k3 = OpenPGPKey(ADDRESS, key_id="3",
+ fingerprint=KEY_FINGERPRINT,
+ refreshed_at=datetime(2001, 1, 1))
+ d1 = self._soledad.create_doc_from_json(k1.get_json())
+ d2 = self._soledad.create_doc_from_json(k2.get_json())
+ d3 = self._soledad.create_doc_from_json(k3.get_json())
+ return gatherResults([d1, d2, d3])
+ return get_from_index(*args)
+
+ self._soledad.get_from_index = my_get_from_index
+ self._soledad.delete_doc = Mock(return_value=succeed(None))
+
+ try:
+ yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS)
+ self.assertEqual(self._soledad.delete_doc.call_count, 2)
+ finally:
+ self._soledad.get_from_index = get_from_index
+ self._soledad.delete_doc = delete_doc
+
def _assert_key_not_found(self, pgp, address, private=False):
d = pgp.get_key(address, private=private)
return self.assertFailure(d, KeyNotFound)
diff --git a/src/leap/keymanager/tests/test_validation.py b/src/leap/keymanager/tests/test_validation.py
index ddf1170..bcf41c4 100644
--- a/src/leap/keymanager/tests/test_validation.py
+++ b/src/leap/keymanager/tests/test_validation.py
@@ -30,6 +30,9 @@ from leap.keymanager.tests import (
KeyManagerWithSoledadTestCase,
ADDRESS,
PUBLIC_KEY,
+ ADDRESS_2,
+ PUBLIC_KEY_2,
+ PRIVATE_KEY_2,
KEY_FINGERPRINT
)
from leap.keymanager.validation import ValidationLevels
@@ -101,7 +104,7 @@ class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase):
self.assertEqual(key.fingerprint, UNRELATED_FINGERPRINT)
@inlineCallbacks
- def test_used(self):
+ def test_used_with_verify(self):
TEXT = "some text"
km = self._key_manager()
@@ -119,6 +122,27 @@ class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase):
yield self.assertFailure(d, KeyNotValidUpgrade)
@inlineCallbacks
+ def test_used_with_decrypt(self):
+ TEXT = "some text"
+
+ km = self._key_manager()
+ yield km.put_raw_key(UNEXPIRED_KEY, OpenPGPKey, ADDRESS)
+ yield km.put_raw_key(PRIVATE_KEY_2, OpenPGPKey, ADDRESS_2)
+ yield km.encrypt(TEXT, ADDRESS, OpenPGPKey)
+
+ km2 = self._key_manager()
+ yield km2.put_raw_key(UNEXPIRED_PRIVATE, OpenPGPKey, ADDRESS)
+ yield km2.put_raw_key(PUBLIC_KEY_2, OpenPGPKey, ADDRESS_2)
+ encrypted = yield km2.encrypt(TEXT, ADDRESS_2, OpenPGPKey,
+ sign=ADDRESS)
+
+ yield km.decrypt(encrypted, ADDRESS_2, OpenPGPKey, verify=ADDRESS)
+ d = km.put_raw_key(
+ UNRELATED_KEY, OpenPGPKey, ADDRESS,
+ validation=ValidationLevels.Provider_Endorsement)
+ yield self.assertFailure(d, KeyNotValidUpgrade)
+
+ @inlineCallbacks
def test_signed_key(self):
km = self._key_manager()
yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS)