diff options
Diffstat (limited to 'src/leap/keymanager')
-rw-r--r-- | src/leap/keymanager/__init__.py | 235 | ||||
-rw-r--r-- | src/leap/keymanager/_version.py | 9 | ||||
-rw-r--r-- | src/leap/keymanager/keys.py | 213 | ||||
-rw-r--r-- | src/leap/keymanager/migrator.py | 181 | ||||
-rw-r--r-- | src/leap/keymanager/openpgp.py | 387 | ||||
-rw-r--r-- | src/leap/keymanager/tests/__init__.py | 31 | ||||
-rw-r--r-- | src/leap/keymanager/tests/test_keymanager.py | 149 | ||||
-rw-r--r-- | src/leap/keymanager/tests/test_migrator.py | 175 | ||||
-rw-r--r-- | src/leap/keymanager/tests/test_openpgp.py | 157 | ||||
-rw-r--r-- | src/leap/keymanager/tests/test_validation.py | 164 | ||||
-rw-r--r-- | src/leap/keymanager/validation.py | 4 |
11 files changed, 1165 insertions, 540 deletions
diff --git a/src/leap/keymanager/__init__.py b/src/leap/keymanager/__init__.py index c7886e0..1106c23 100644 --- a/src/leap/keymanager/__init__.py +++ b/src/leap/keymanager/__init__.py @@ -22,6 +22,8 @@ import fileinput import os import sys import tempfile +import json +import urllib from leap.common import ca_bundle @@ -61,6 +63,7 @@ from twisted.internet import defer from urlparse import urlparse from leap.common.check import leap_assert +from leap.common.http import HTTPClient from leap.common.events import emit_async, catalog from leap.common.decorators import memoized_method @@ -141,6 +144,8 @@ class KeyManager(object): # the following are used to perform https requests self._fetcher = requests self._combined_ca_bundle = self._create_combined_bundle_file() + self._async_client = HTTPClient(self._combined_ca_bundle) + self._async_client_pinned = HTTPClient(self._ca_cert_path) # # destructor @@ -179,40 +184,55 @@ class KeyManager(object): def _key_class_from_type(self, ktype): """ - Return key class from string representation of key type. + Given a class type, return a class + + :param ktype: string representation of a class name + :type ktype: str + + :return: A class with the matching name + :rtype: classobj or type """ return filter( lambda klass: klass.__name__ == ktype, self._wrapper_map).pop() - def _get(self, uri, data=None): + @defer.inlineCallbacks + def _get_key_from_nicknym(self, address): """ Send a GET request to C{uri} containing C{data}. - :param uri: The URI of the request. - :type uri: str - :param data: The body of the request. - :type data: dict, str or file + :param address: The URI of the request. + :type address: str - :return: The response to the request. - :rtype: requests.Response + :return: A deferred that will be fired with GET content as json (dict) + :rtype: Deferred """ - leap_assert( - self._ca_cert_path is not None, - 'We need the CA certificate path!') - res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path) - # Nickserver now returns 404 for key not found and 500 for - # other cases (like key too small), so we are skipping this - # check for the time being - # res.raise_for_status() + try: + uri = self._nickserver_uri + '?address=' + address + content = yield self._async_client_pinned.request(str(uri), 'GET') + json_content = json.loads(content) + except IOError as e: + # FIXME: 404 doesnt raise today, but it wont produce json anyway + # if e.response.status_code == 404: + # raise KeyNotFound(address) + logger.warning("HTTP error retrieving key: %r" % (e,)) + logger.warning("%s" % (content,)) + raise KeyNotFound(e.message), None, sys.exc_info()[2] + except ValueError as v: + logger.warning("Invalid JSON data from key: %s" % (uri,)) + raise KeyNotFound(v.message + ' - ' + uri), None, sys.exc_info()[2] + except Exception as e: + logger.warning("Error retrieving key: %r" % (e,)) + raise KeyNotFound(e.message), None, sys.exc_info()[2] # Responses are now text/plain, although it's json anyway, but # this will fail when it shouldn't # leap_assert( # res.headers['content-type'].startswith('application/json'), # 'Content-type is not JSON.') - return res + defer.returnValue(json_content) + @defer.inlineCallbacks def _get_with_combined_ca_bundle(self, uri, data=None): """ Send a GET request to C{uri} containing C{data}. @@ -225,12 +245,19 @@ class KeyManager(object): :param data: The body of the request. :type data: dict, str or file - :return: The response to the request. - :rtype: requests.Response + :return: A deferred that will be fired with the GET response + :rtype: Deferred """ - return self._fetcher.get( - uri, data=data, verify=self._combined_ca_bundle) + try: + content = yield self._async_client.request(str(uri), 'GET') + except Exception as e: + logger.warning("There was a problem fetching key: %s" % (e,)) + raise KeyNotFound(uri) + if not content: + raise KeyNotFound(uri) + defer.returnValue(content) + @defer.inlineCallbacks def _put(self, uri, data=None): """ Send a PUT request to C{uri} containing C{data}. @@ -244,23 +271,31 @@ class KeyManager(object): :param data: The body of the request. :type data: dict, str or file - :return: The response to the request. - :rtype: requests.Response + :return: A deferred that will be fired when PUT request finishes + :rtype: Deferred """ leap_assert( - self._ca_cert_path is not None, - 'We need the CA certificate path!') - leap_assert( self._token is not None, 'We need a token to interact with webapp!') - res = self._fetcher.put( - uri, data=data, verify=self._ca_cert_path, - headers={'Authorization': 'Token token=%s' % self._token}) - # assert that the response is valid - res.raise_for_status() - return res + if type(data) == dict: + data = urllib.urlencode(data) + headers = {'Authorization': [str('Token token=%s' % self._token)]} + headers['Content-Type'] = ['application/x-www-form-urlencoded'] + try: + res = yield self._async_client_pinned.request(str(uri), 'PUT', + body=str(data), + headers=headers) + except Exception as e: + logger.warning("Error uploading key: %r" % (e,)) + raise e + if 'error' in res: + # FIXME: That's a workaround for 500, + # we need to implement a readBody to assert response code + logger.warning("Error uploading key: %r" % (res,)) + raise Exception(res) @memoized_method(invalidation=300) + @defer.inlineCallbacks def _fetch_keys_from_server(self, address): """ Fetch keys bound to address from nickserver and insert them in @@ -276,38 +311,22 @@ class KeyManager(object): """ # request keys from the nickserver - d = defer.succeed(None) - res = None - try: - res = self._get(self._nickserver_uri, {'address': address}) - res.raise_for_status() - server_keys = res.json() - - # insert keys in local database - if self.OPENPGP_KEY in server_keys: - # nicknym server is authoritative for its own domain, - # for other domains the key might come from key servers. - validation_level = ValidationLevels.Weak_Chain - _, domain = _split_email(address) - if (domain == _get_domain(self._nickserver_uri)): - validation_level = ValidationLevels.Provider_Trust - - d = self.put_raw_key( - server_keys['openpgp'], - OpenPGPKey, - address=address, - validation=validation_level) - except requests.exceptions.HTTPError as e: - if e.response.status_code == 404: - d = defer.fail(KeyNotFound(address)) - else: - d = defer.fail(KeyNotFound(e.message)) - logger.warning("HTTP error retrieving key: %r" % (e,)) - logger.warning("%s" % (res.content,)) - except Exception as e: - d = defer.fail(KeyNotFound(e.message)) - logger.warning("Error retrieving key: %r" % (e,)) - return d + server_keys = yield self._get_key_from_nicknym(address) + + # insert keys in local database + if self.OPENPGP_KEY in server_keys: + # nicknym server is authoritative for its own domain, + # for other domains the key might come from key servers. + validation_level = ValidationLevels.Weak_Chain + _, domain = _split_email(address) + if (domain == _get_domain(self._nickserver_uri)): + validation_level = ValidationLevels.Provider_Trust + + yield self.put_raw_key( + server_keys['openpgp'], + OpenPGPKey, + address=address, + validation=validation_level) # # key management @@ -339,8 +358,11 @@ class KeyManager(object): self._api_uri, self._api_version, self._uid) - self._put(uri, data) - emit_async(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, self._address) + d = self._put(uri, data) + d.addCallback(lambda _: + emit_async(catalog.KEYMANAGER_DONE_UPLOADING_KEYS, + self._address)) + return d d = self.get_key( self._address, ktype, private=False, fetch_remote=False) @@ -417,6 +439,7 @@ class KeyManager(object): :return: A Deferred which fires with a list of all keys in local db. :rtype: Deferred """ + # TODO: should it be based on activedocs? def build_keys(docs): return map( lambda doc: build_key_from_dict( @@ -550,15 +573,16 @@ class KeyManager(object): self._assert_supported_key_type(ktype) _keys = self._wrapper_map[ktype] + @defer.inlineCallbacks def encrypt(keys): pubkey, signkey = keys - encrypted = _keys.encrypt( + encrypted = yield _keys.encrypt( data, pubkey, passphrase, sign=signkey, cipher_algo=cipher_algo) - pubkey.encr_used = True - d = _keys.put_key(pubkey, address) - d.addCallback(lambda _: encrypted) - return d + if not pubkey.encr_used: + pubkey.encr_used = True + yield _keys.put_key(pubkey) + defer.returnValue(encrypted) dpub = self.get_key(address, ktype, private=False, fetch_remote=fetch_remote) @@ -603,9 +627,10 @@ class KeyManager(object): self._assert_supported_key_type(ktype) _keys = self._wrapper_map[ktype] + @defer.inlineCallbacks def decrypt(keys): pubkey, privkey = keys - decrypted, signed = _keys.decrypt( + decrypted, signed = yield _keys.decrypt( data, privkey, passphrase=passphrase, verify=pubkey) if pubkey is None: signature = KeyNotFound(verify) @@ -613,14 +638,13 @@ class KeyManager(object): signature = pubkey if not pubkey.sign_used: pubkey.sign_used = True - d = _keys.put_key(pubkey, verify) - d.addCallback(lambda _: (decrypted, signature)) - return d + yield _keys.put_key(pubkey) + defer.returnValue((decrypted, signature)) else: signature = InvalidSignature( 'Failed to verify signature with key %s' % - (pubkey.key_id,)) - return (decrypted, signature) + (pubkey.fingerprint,)) + defer.returnValue((decrypted, signature)) dpriv = self.get_key(address, ktype, private=True) dpub = defer.succeed(None) @@ -711,14 +735,14 @@ class KeyManager(object): if signed: if not pubkey.sign_used: pubkey.sign_used = True - d = _keys.put_key(pubkey, address) + d = _keys.put_key(pubkey) d.addCallback(lambda _: pubkey) return d return pubkey else: raise InvalidSignature( 'Failed to verify signature with key %s' % - (pubkey.key_id,)) + (pubkey.fingerprint,)) d = self.get_key(address, ktype, private=False, fetch_remote=fetch_remote) @@ -742,20 +766,16 @@ class KeyManager(object): _keys = self._wrapper_map[type(key)] return _keys.delete_key(key) - def put_key(self, key, address): + def put_key(self, key): """ Put key bound to address in local storage. :param key: The key to be stored :type key: EncryptionKey - :param address: address for which this key will be active - :type address: str :return: A Deferred which fires when the key is in the storage, or - which fails with KeyAddressMismatch if address doesn't match - any uid on the key or fails with KeyNotValidUpdate if a key - with the same uid exists and the new one is not a valid update - for it. + which fails with KeyNotValidUpdate if a key with the same + uid exists and the new one is not a valid update for it. :rtype: Deferred :raise UnsupportedKeyTypeError: if invalid key type @@ -764,11 +784,6 @@ class KeyManager(object): self._assert_supported_key_type(ktype) _keys = self._wrapper_map[ktype] - if address not in key.address: - return defer.fail( - KeyAddressMismatch("UID %s found, but expected %s" - % (str(key.address), address))) - def old_key_not_found(failure): if failure.check(KeyNotFound): return None @@ -777,13 +792,13 @@ class KeyManager(object): def check_upgrade(old_key): if key.private or can_upgrade(key, old_key): - return _keys.put_key(key, address) + return _keys.put_key(key) else: raise KeyNotValidUpgrade( "Key %s can not be upgraded by new key %s" - % (old_key.key_id, key.key_id)) + % (old_key.fingerprint, key.fingerprint)) - d = _keys.get_key(address, private=key.private) + d = _keys.get_key(key.address, private=key.private) d.addErrback(old_key_not_found) d.addCallback(check_upgrade) return d @@ -805,9 +820,10 @@ class KeyManager(object): :return: A Deferred which fires when the key is in the storage, or which fails with KeyAddressMismatch if address doesn't match - any uid on the key or fails with KeyNotValidUpdate if a key - with the same uid exists and the new one is not a valid update - for it. + any uid on the key or fails with KeyNotFound if no OpenPGP + material was found in key or fails with KeyNotValidUpdate if a + key with the same uid exists and the new one is not a valid + update for it. :rtype: Deferred :raise UnsupportedKeyTypeError: if invalid key type @@ -815,13 +831,18 @@ class KeyManager(object): self._assert_supported_key_type(ktype) _keys = self._wrapper_map[ktype] - pubkey, privkey = _keys.parse_ascii_key(key) + pubkey, privkey = _keys.parse_ascii_key(key, address) + + if pubkey is None: + return defer.fail(KeyNotFound(key)) + pubkey.validation = validation - d = self.put_key(pubkey, address) + d = self.put_key(pubkey) if privkey is not None: - d.addCallback(lambda _: self.put_key(privkey, address)) + d.addCallback(lambda _: self.put_key(privkey)) return d + @defer.inlineCallbacks def fetch_key(self, address, uri, ktype, validation=ValidationLevels.Weak_Chain): """ @@ -851,21 +872,15 @@ class KeyManager(object): _keys = self._wrapper_map[ktype] 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)) + ascii_content = yield self._get_with_combined_ca_bundle(uri) # XXX parse binary keys - pubkey, _ = _keys.parse_ascii_key(res.content) + pubkey, _ = _keys.parse_ascii_key(ascii_content, address) if pubkey is None: - return defer.fail(KeyNotFound(uri)) + raise KeyNotFound(uri) pubkey.validation = validation - return self.put_key(pubkey, address) + yield self.put_key(pubkey) def _assert_supported_key_type(self, ktype): """ diff --git a/src/leap/keymanager/_version.py b/src/leap/keymanager/_version.py index 049099d..8e7e599 100644 --- a/src/leap/keymanager/_version.py +++ b/src/leap/keymanager/_version.py @@ -1,13 +1,14 @@ # This file was generated by the `freeze_debianver` command in setup.py -# Using 'versioneer.py' (0.7+) from +# Using 'versioneer.py' (0.16) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. -version_version = '0.4.3' -version_full = 'f17f8c43e217b65a951e5db357699c2d2af6098f' +version_version = '0.5.0' +full_revisionid = '0f600595dd2d26bc16174435dbf0c3accd690982' def get_versions(default={}, verbose=False): - return {'version': version_version, 'full': version_full} + return {'version': version_version, + 'full-revisionid': full_revisionid} diff --git a/src/leap/keymanager/keys.py b/src/leap/keymanager/keys.py index 91559c2..1955d54 100644 --- a/src/leap/keymanager/keys.py +++ b/src/leap/keymanager/keys.py @@ -28,6 +28,7 @@ except ImportError: import logging import re import time +import traceback from abc import ABCMeta, abstractmethod @@ -44,9 +45,10 @@ logger = logging.getLogger(__name__) # Dictionary keys used for storing cryptographic keys. # +KEY_VERSION_KEY = 'version' +KEY_UIDS_KEY = 'uids' KEY_ADDRESS_KEY = 'address' KEY_TYPE_KEY = 'type' -KEY_ID_KEY = 'key_id' KEY_FINGERPRINT_KEY = 'fingerprint' KEY_DATA_KEY = 'key_data' KEY_PRIVATE_KEY = 'private' @@ -68,22 +70,26 @@ KEYMANAGER_KEY_TAG = 'keymanager-key' KEYMANAGER_ACTIVE_TAG = 'keymanager-active' KEYMANAGER_ACTIVE_TYPE = '-active' +# Version of the Soledad Document schema, +# it should be bumped each time the document format changes +KEYMANAGER_DOC_VERSION = 1 + # # key indexing constants. # TAGS_PRIVATE_INDEX = 'by-tags-private' -TYPE_ID_PRIVATE_INDEX = 'by-type-id-private' +TYPE_FINGERPRINT_PRIVATE_INDEX = 'by-type-fingerprint-private' TYPE_ADDRESS_PRIVATE_INDEX = 'by-type-address-private' INDEXES = { TAGS_PRIVATE_INDEX: [ KEY_TAGS_KEY, 'bool(%s)' % KEY_PRIVATE_KEY, ], - TYPE_ID_PRIVATE_INDEX: [ + TYPE_FINGERPRINT_PRIVATE_INDEX: [ KEY_TYPE_KEY, - KEY_ID_KEY, + KEY_FINGERPRINT_KEY, 'bool(%s)' % KEY_PRIVATE_KEY, ], TYPE_ADDRESS_PRIVATE_INDEX: [ @@ -110,39 +116,51 @@ def is_address(address): return bool(re.match('[\w.-]+@[\w.-]+', address)) -def build_key_from_dict(kClass, kdict): +def build_key_from_dict(kClass, key, active=None): """ Build an C{kClass} key based on info in C{kdict}. - :param kdict: Dictionary with key data. - :type kdict: dict + :param key: Dictionary with key data. + :type key: dict + :param active: Dictionary with active data. + :type active: dict :return: An instance of the key. :rtype: C{kClass} """ - try: - validation = ValidationLevels.get(kdict[KEY_VALIDATION_KEY]) - except ValueError: - logger.error("Not valid validation level (%s) for key %s", - (kdict[KEY_VALIDATION_KEY], kdict[KEY_ID_KEY])) - validation = ValidationLevels.Weak_Chain - - expiry_date = _to_datetime(kdict[KEY_EXPIRY_DATE_KEY]) - last_audited_at = _to_datetime(kdict[KEY_LAST_AUDITED_AT_KEY]) - refreshed_at = _to_datetime(kdict[KEY_REFRESHED_AT_KEY]) + address = None + validation = ValidationLevels.Weak_Chain + last_audited_at = None + encr_used = False + sign_used = False + + if active: + address = active[KEY_ADDRESS_KEY] + try: + validation = ValidationLevels.get(active[KEY_VALIDATION_KEY]) + except ValueError: + logger.error("Not valid validation level (%s) for key %s", + (active[KEY_VALIDATION_KEY], + active[KEY_FINGERPRINT_KEY])) + last_audited_at = _to_datetime(active[KEY_LAST_AUDITED_AT_KEY]) + encr_used = active[KEY_ENCR_USED_KEY] + sign_used = active[KEY_SIGN_USED_KEY] + + expiry_date = _to_datetime(key[KEY_EXPIRY_DATE_KEY]) + refreshed_at = _to_datetime(key[KEY_REFRESHED_AT_KEY]) return kClass( - kdict[KEY_ADDRESS_KEY], - key_id=kdict[KEY_ID_KEY], - fingerprint=kdict[KEY_FINGERPRINT_KEY], - key_data=kdict[KEY_DATA_KEY], - private=kdict[KEY_PRIVATE_KEY], - length=kdict[KEY_LENGTH_KEY], + address=address, + uids=key[KEY_UIDS_KEY], + fingerprint=key[KEY_FINGERPRINT_KEY], + key_data=key[KEY_DATA_KEY], + private=key[KEY_PRIVATE_KEY], + length=key[KEY_LENGTH_KEY], expiry_date=expiry_date, last_audited_at=last_audited_at, refreshed_at=refreshed_at, validation=validation, - encr_used=kdict[KEY_ENCR_USED_KEY], - sign_used=kdict[KEY_SIGN_USED_KEY], + encr_used=encr_used, + sign_used=sign_used, ) @@ -174,22 +192,32 @@ class EncryptionKey(object): __metaclass__ = ABCMeta - def __init__(self, address, key_id="", fingerprint="", + __slots__ = ('address', 'uids', 'fingerprint', 'key_data', + 'private', 'length', 'expiry_date', 'validation', + 'last_audited_at', 'refreshed_at', + 'encr_used', 'sign_used', '_index') + + def __init__(self, address=None, uids=[], fingerprint="", key_data="", private=False, length=0, expiry_date=None, validation=ValidationLevels.Weak_Chain, last_audited_at=None, refreshed_at=None, encr_used=False, sign_used=False): self.address = address - self.key_id = key_id + if not uids and address: + self.uids = [address] + else: + self.uids = uids self.fingerprint = fingerprint self.key_data = key_data self.private = private self.length = length self.expiry_date = expiry_date + self.validation = validation self.last_audited_at = last_audited_at self.refreshed_at = refreshed_at self.encr_used = encr_used self.sign_used = sign_used + self._index = len(self.__slots__) def get_json(self): """ @@ -199,50 +227,71 @@ class EncryptionKey(object): :rtype: str """ expiry_date = _to_unix_time(self.expiry_date) - last_audited_at = _to_unix_time(self.last_audited_at) refreshed_at = _to_unix_time(self.refreshed_at) return json.dumps({ - KEY_ADDRESS_KEY: self.address, + KEY_UIDS_KEY: self.uids, KEY_TYPE_KEY: self.__class__.__name__, - KEY_ID_KEY: self.key_id, KEY_FINGERPRINT_KEY: self.fingerprint, KEY_DATA_KEY: self.key_data, KEY_PRIVATE_KEY: self.private, KEY_LENGTH_KEY: self.length, KEY_EXPIRY_DATE_KEY: expiry_date, - KEY_LAST_AUDITED_AT_KEY: last_audited_at, KEY_REFRESHED_AT_KEY: refreshed_at, - KEY_VALIDATION_KEY: str(self.validation), - KEY_ENCR_USED_KEY: self.encr_used, - KEY_SIGN_USED_KEY: self.sign_used, + KEY_VERSION_KEY: KEYMANAGER_DOC_VERSION, KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG], }) - def get_active_json(self, address): + def get_active_json(self): """ Return a JSON string describing this key. - :param address: Address for wich the key is active - :type address: str :return: The JSON string describing this key. :rtype: str """ + last_audited_at = _to_unix_time(self.last_audited_at) + return json.dumps({ - KEY_ADDRESS_KEY: address, + KEY_ADDRESS_KEY: self.address, KEY_TYPE_KEY: self.__class__.__name__ + KEYMANAGER_ACTIVE_TYPE, - KEY_ID_KEY: self.key_id, + KEY_FINGERPRINT_KEY: self.fingerprint, KEY_PRIVATE_KEY: self.private, + KEY_VALIDATION_KEY: str(self.validation), + KEY_LAST_AUDITED_AT_KEY: last_audited_at, + KEY_ENCR_USED_KEY: self.encr_used, + KEY_SIGN_USED_KEY: self.sign_used, + KEY_VERSION_KEY: KEYMANAGER_DOC_VERSION, KEY_TAGS_KEY: [KEYMANAGER_ACTIVE_TAG], }) + def next(self): + if self._index == 0: + self._index = len(self.__slots__) + raise StopIteration + + self._index -= 1 + key = self.__slots__[self._index] + + if key.startswith('_'): + return self.next() + + value = getattr(self, key) + if key == "validation": + value = str(value) + elif key in ["expiry_date", "last_audited_at", "refreshed_at"]: + value = str(value) + return key, value + + def __iter__(self): + return self + def __repr__(self): """ Representation of this class """ return u"<%s 0x%s (%s - %s)>" % ( self.__class__.__name__, - self.key_id, + self.fingerprint, self.address, "priv" if self.private else "publ") @@ -270,7 +319,8 @@ class EncryptionScheme(object): :type soledad: leap.soledad.Soledad """ self._soledad = soledad - self._init_indexes() + self.deferred_init = self._init_indexes() + self.deferred_init.addCallback(self._migrate_documents_schema) def _init_indexes(self): """ @@ -298,8 +348,14 @@ class EncryptionScheme(object): deferreds.append(d) return defer.gatherResults(deferreds, consumeErrors=True) - self.deferred_indexes = self._soledad.list_indexes() - self.deferred_indexes.addCallback(init_idexes) + d = self._soledad.list_indexes() + d.addCallback(init_idexes) + return d + + def _migrate_documents_schema(self, _): + from leap.keymanager.migrator import KeyDocumentsMigrator + migrator = KeyDocumentsMigrator(self._soledad) + return migrator.migrate() def _wait_indexes(self, *methods): """ @@ -332,7 +388,7 @@ class EncryptionScheme(object): self.stored[method] = getattr(self, method) setattr(self, method, makeWrapper(method)) - self.deferred_indexes.addCallback(restore) + self.deferred_init.addCallback(restore) @abstractmethod def get_key(self, address, private=False): @@ -352,14 +408,12 @@ class EncryptionScheme(object): pass @abstractmethod - def put_key(self, key, address): + def put_key(self, key): """ Put a key in local storage. :param key: The key to be stored. :type key: EncryptionKey - :param address: address for which this key will be active. - :type address: str :return: A Deferred which fires when the key is in the storage. :rtype: Deferred @@ -464,3 +518,68 @@ class EncryptionScheme(object): :rtype: bool """ pass + + def _repair_key_docs(self, doclist): + """ + If there is more than one key for a key id try to self-repair it + + :return: a Deferred that will be fired with the valid key doc once all + the deletions are completed + :rtype: Deferred + """ + def log_key_doc(doc): + logger.error("\t%s: %s" % (doc.content[KEY_UIDS_KEY], + doc.content[KEY_FINGERPRINT_KEY])) + + def cmp_key(d1, d2): + return cmp(d1.content[KEY_REFRESHED_AT_KEY], + d2.content[KEY_REFRESHED_AT_KEY]) + + return self._repair_docs(doclist, cmp_key, log_key_doc) + + def _repair_active_docs(self, doclist): + """ + If there is more than one active doc for an address try to self-repair + it + + :return: a Deferred that will be fired with the valid active doc once + all the deletions are completed + :rtype: Deferred + """ + def log_active_doc(doc): + logger.error("\t%s: %s" % (doc.content[KEY_ADDRESS_KEY], + doc.content[KEY_FINGERPRINT_KEY])) + + def cmp_active(d1, d2): + res = cmp(d1.content[KEY_LAST_AUDITED_AT_KEY], + d2.content[KEY_LAST_AUDITED_AT_KEY]) + if res != 0: + return res + + used1 = (d1.content[KEY_SIGN_USED_KEY] + + d1.content[KEY_ENCR_USED_KEY]) + used2 = (d2.content[KEY_SIGN_USED_KEY] + + d2.content[KEY_ENCR_USED_KEY]) + return cmp(used1, used2) + + return self._repair_docs(doclist, cmp_active, log_active_doc) + + def _repair_docs(self, doclist, cmp_func, log_func): + logger.error("BUG ---------------------------------------------------") + logger.error("There is more than one doc of type %s:" + % (doclist[0].content[KEY_TYPE_KEY],)) + + doclist.sort(cmp=cmp_func, reverse=True) + log_func(doclist[0]) + deferreds = [] + for doc in doclist[1:]: + log_func(doc) + d = self._soledad.delete_doc(doc) + deferreds.append(d) + + logger.error("") + logger.error(traceback.extract_stack()) + logger.error("BUG (please report above info) ------------------------") + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: doclist[0]) + return d diff --git a/src/leap/keymanager/migrator.py b/src/leap/keymanager/migrator.py new file mode 100644 index 0000000..9e4ae77 --- /dev/null +++ b/src/leap/keymanager/migrator.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# migrator.py +# Copyright (C) 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +Document migrator +""" +# XXX: versioning has being added 12/2015 when keymanager was not +# much in use in the wild. We can probably drop support for +# keys without version at some point. + + +from collections import namedtuple +from twisted.internet.defer import gatherResults, succeed + +from leap.keymanager.keys import ( + TAGS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + KEYMANAGER_ACTIVE_TAG, + + KEYMANAGER_DOC_VERSION, + KEY_ADDRESS_KEY, + KEY_UIDS_KEY, + KEY_VERSION_KEY, + KEY_FINGERPRINT_KEY, + KEY_VALIDATION_KEY, + KEY_LAST_AUDITED_AT_KEY, + KEY_ENCR_USED_KEY, + KEY_SIGN_USED_KEY, +) +from leap.keymanager.validation import ValidationLevels + + +KEY_ID_KEY = 'key_id' + +KeyDocs = namedtuple("KeyDocs", ['key', 'active']) + + +class KeyDocumentsMigrator(object): + """ + Migrate old KeyManager Soledad Documents to the newest schema + """ + + def __init__(self, soledad): + self._soledad = soledad + + def migrate(self): + deferred_public = self._get_docs(private=False) + deferred_public.addCallback(self._migrate_docs) + + deferred_private = self._get_docs(private=True) + deferred_private.addCallback(self._migrate_docs) + + return gatherResults([deferred_public, deferred_private]) + + def _get_docs(self, private=False): + private_value = '1' if private else '0' + + deferred_keys = self._soledad.get_from_index( + TAGS_PRIVATE_INDEX, + KEYMANAGER_KEY_TAG, + private_value) + deferred_active = self._soledad.get_from_index( + TAGS_PRIVATE_INDEX, + KEYMANAGER_ACTIVE_TAG, + private_value) + return gatherResults([deferred_keys, deferred_active]) + + def _migrate_docs(self, (key_docs, active_docs)): + def update_keys(keys): + deferreds = [] + for key_id in keys: + key = keys[key_id].key + actives = keys[key_id].active + + d = self._migrate_actives(key, actives) + deferreds.append(d) + + d = self._migrate_key(key) + deferreds.append(d) + return gatherResults(deferreds) + + d = self._buildKeyDict(key_docs, active_docs) + d.addCallback(lambda keydict: self._filter_outdated(keydict)) + d.addCallback(update_keys) + + def _buildKeyDict(self, keys, actives): + keydict = { + fp2id(key.content[KEY_FINGERPRINT_KEY]): KeyDocs(key, []) + for key in keys} + + deferreds = [] + for active in actives: + if KEY_ID_KEY in active.content: + key_id = active.content[KEY_ID_KEY] + if key_id not in keydict: + d = self._soledad.delete_doc(active) + deferreds.append(d) + continue + keydict[key_id].active.append(active) + + d = gatherResults(deferreds) + d.addCallback(lambda _: keydict) + return d + + def _filter_outdated(self, keydict): + outdated = {} + for key_id, docs in keydict.items(): + if ((docs.key and KEY_VERSION_KEY not in docs.key.content) or + docs.active): + outdated[key_id] = docs + return outdated + + def _migrate_actives(self, key, actives): + if not key: + deferreds = [] + for active in actives: + d = self._soledad.delete_doc(active) + deferreds.append(d) + return gatherResults(deferreds) + + validation = str(ValidationLevels.Weak_Chain) + last_audited = 0 + encr_used = False + sign_used = False + fingerprint = key.content[KEY_FINGERPRINT_KEY] + if len(actives) == 1 and KEY_VERSION_KEY not in key.content: + # we can preserve the validation of the key if there is only one + # active address for the key + validation = key.content[KEY_VALIDATION_KEY] + last_audited = key.content[KEY_LAST_AUDITED_AT_KEY] + encr_used = key.content[KEY_ENCR_USED_KEY] + sign_used = key.content[KEY_SIGN_USED_KEY] + + deferreds = [] + for active in actives: + if KEY_VERSION_KEY in active.content: + continue + + active.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION + active.content[KEY_FINGERPRINT_KEY] = fingerprint + active.content[KEY_VALIDATION_KEY] = validation + active.content[KEY_LAST_AUDITED_AT_KEY] = last_audited + active.content[KEY_ENCR_USED_KEY] = encr_used + active.content[KEY_SIGN_USED_KEY] = sign_used + del active.content[KEY_ID_KEY] + d = self._soledad.put_doc(active) + deferreds.append(d) + return gatherResults(deferreds) + + def _migrate_key(self, key): + if not key or KEY_VERSION_KEY in key.content: + return succeed(None) + + key.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION + key.content[KEY_UIDS_KEY] = key.content[KEY_ADDRESS_KEY] + del key.content[KEY_ADDRESS_KEY] + del key.content[KEY_ID_KEY] + del key.content[KEY_VALIDATION_KEY] + del key.content[KEY_LAST_AUDITED_AT_KEY] + del key.content[KEY_ENCR_USED_KEY] + del key.content[KEY_SIGN_USED_KEY] + return self._soledad.put_doc(key) + + +def fp2id(fingerprint): + KEY_ID_LENGTH = 16 + return fingerprint[-KEY_ID_LENGTH:] diff --git a/src/leap/keymanager/openpgp.py b/src/leap/keymanager/openpgp.py index d648137..a843261 100644 --- a/src/leap/keymanager/openpgp.py +++ b/src/leap/keymanager/openpgp.py @@ -22,14 +22,15 @@ import os import re import shutil import tempfile -import traceback import io from datetime import datetime +from multiprocessing import cpu_count from gnupg import GPG from gnupg.gnupg import GPGUtilities from twisted.internet import defer +from twisted.internet.threads import deferToThread from leap.common.check import leap_assert, leap_assert_type, leap_check from leap.keymanager import errors @@ -38,12 +39,10 @@ from leap.keymanager.keys import ( EncryptionScheme, is_address, build_key_from_dict, - TYPE_ID_PRIVATE_INDEX, + TYPE_FINGERPRINT_PRIVATE_INDEX, TYPE_ADDRESS_PRIVATE_INDEX, - KEY_ADDRESS_KEY, - KEY_ID_KEY, + KEY_UIDS_KEY, KEY_FINGERPRINT_KEY, - KEY_REFRESHED_AT_KEY, KEYMANAGER_ACTIVE_TYPE, ) @@ -55,6 +54,16 @@ logger = logging.getLogger(__name__) # A temporary GPG keyring wrapped to provide OpenPGP functionality. # +# This function will be used to call blocking GPG functions outside +# of Twisted reactor and match the concurrent calls to the amount of CPU cores +cpu_core_semaphore = defer.DeferredSemaphore(cpu_count()) + + +def from_thread(func, *args, **kwargs): + call = lambda: deferToThread(func, *args, **kwargs) + return cpu_core_semaphore.run(call) + + class TempGPGWrapper(object): """ A context manager that wraps a temporary GPG keyring which only contains @@ -113,9 +122,9 @@ class TempGPGWrapper(object): # itself is enough to also have the public key in the keyring, # and we want to count the keys afterwards. - privids = map(lambda privkey: privkey.key_id, privkeys) + privfps = map(lambda privkey: privkey.fingerprint, privkeys) publkeys = filter( - lambda pubkey: pubkey.key_id not in privids, publkeys) + lambda pubkey: pubkey.fingerprint not in privfps, publkeys) listkeys = lambda: self._gpg.list_keys() listsecretkeys = lambda: self._gpg.list_keys(secret=True) @@ -191,7 +200,7 @@ class OpenPGPKey(EncryptionKey): Base class for OpenPGP keys. """ - def __init__(self, address, gpgbinary=None, **kwargs): + def __init__(self, address=None, gpgbinary=None, **kwargs): self._gpgbinary = gpgbinary super(OpenPGPKey, self).__init__(address, **kwargs) @@ -204,13 +213,48 @@ class OpenPGPKey(EncryptionKey): :rtype: list(str) """ with TempGPGWrapper(keys=[self], gpgbinary=self._gpgbinary) as gpg: - res = gpg.list_sigs(self.key_id) + res = gpg.list_sigs(self.fingerprint) for uid, sigs in res.sigs.iteritems(): - if _parse_address(uid) in self.address: + if _parse_address(uid) in self.uids: return sigs return [] + def merge(self, newkey): + if newkey.fingerprint != self.fingerprint: + logger.critical( + "Can't put a key whith the same key_id and different " + "fingerprint: %s, %s" + % (newkey.fingerprint, self.fingerprint)) + raise errors.KeyFingerprintMismatch(newkey.fingerprint) + + with TempGPGWrapper(gpgbinary=self._gpgbinary) as gpg: + gpg.import_keys(self.key_data) + gpg.import_keys(newkey.key_data) + gpgkey = gpg.list_keys(secret=newkey.private).pop() + + if gpgkey['expires']: + self.expiry_date = datetime.fromtimestamp( + int(gpgkey['expires'])) + else: + self.expiry_date = None + + self.uids = [] + for uid in gpgkey['uids']: + self.uids.append(_parse_address(uid)) + + self.length = int(gpgkey['length']) + self.key_data = gpg.export_keys(gpgkey['fingerprint'], + secret=self.private) + + if newkey.validation > self.validation: + self.validation = newkey.validation + if newkey.last_audited_at > self.last_audited_at: + self.validation = newkey.last_audited_at + self.encr_used = newkey.encr_used or self.encr_used + self.sign_used = newkey.sign_used or self.sign_used + self.refreshed_at = datetime.now() + class OpenPGPScheme(EncryptionScheme): """ @@ -253,6 +297,7 @@ class OpenPGPScheme(EncryptionScheme): # make sure the key does not already exist leap_assert(is_address(address), 'Not an user address: %s' % address) + @defer.inlineCallbacks def _gen_key(_): with TempGPGWrapper(gpgbinary=self._gpgbinary) as gpg: # TODO: inspect result, or use decorator @@ -264,7 +309,7 @@ class OpenPGPScheme(EncryptionScheme): name_comment='') logger.info("About to generate keys... " "This might take SOME time.") - gpg.gen_key(params) + yield from_thread(gpg.gen_key, params) logger.info("Keys for %s have been successfully " "generated." % (address,)) pubkeys = gpg.list_keys() @@ -290,10 +335,11 @@ class OpenPGPScheme(EncryptionScheme): key = gpg.list_keys(secret=secret).pop() openpgp_key = self._build_key_from_gpg( key, - gpg.export_keys(key['fingerprint'], secret=secret)) - d = self.put_key(openpgp_key, address) + gpg.export_keys(key['fingerprint'], secret=secret), + address) + d = self.put_key(openpgp_key) deferreds.append(d) - return defer.gatherResults(deferreds) + yield defer.gatherResults(deferreds) def key_already_exists(_): raise errors.KeyAlreadyExists(address) @@ -319,15 +365,16 @@ class OpenPGPScheme(EncryptionScheme): """ address = _parse_address(address) - def build_key(doc): - if doc is None: + def build_key((keydoc, activedoc)): + if keydoc is None: raise errors.KeyNotFound(address) leap_assert( - address in doc.content[KEY_ADDRESS_KEY], + address in keydoc.content[KEY_UIDS_KEY], '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) + % (keydoc.content[KEY_FINGERPRINT_KEY], address, + keydoc.content[KEY_UIDS_KEY])) + key = build_key_from_dict(OpenPGPKey, keydoc.content, + activedoc.content) key._gpgbinary = self._gpgbinary return key @@ -335,13 +382,15 @@ class OpenPGPScheme(EncryptionScheme): d.addCallback(build_key) return d - def parse_ascii_key(self, key_data): + def parse_ascii_key(self, key_data, address=None): """ Parses an ascii armored key (or key pair) data and returns the OpenPGPKey keys. :param key_data: the key data to be parsed. :type key_data: str or unicode + :param address: Active address for the key. + :type address: str :returns: the public key and private key (if applies) for that data. :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey) @@ -362,12 +411,13 @@ class OpenPGPScheme(EncryptionScheme): openpgp_privkey = None if privkey: # build private key - openpgp_privkey = self._build_key_from_gpg(priv_info, privkey) + openpgp_privkey = self._build_key_from_gpg(priv_info, privkey, + address) 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) + openpgp_pubkey = self._build_key_from_gpg(pub_info, pubkey, address) return (openpgp_pubkey, openpgp_privkey) @@ -387,12 +437,13 @@ class OpenPGPScheme(EncryptionScheme): openpgp_privkey = None try: - openpgp_pubkey, openpgp_privkey = self.parse_ascii_key(key_data) + openpgp_pubkey, openpgp_privkey = self.parse_ascii_key( + key_data, address) except (errors.KeyAddressMismatch, errors.KeyFingerprintMismatch) as e: return defer.fail(e) def put_key(_, key): - return self.put_key(key, address) + return self.put_key(key) d = defer.succeed(None) if openpgp_pubkey is not None: @@ -401,112 +452,59 @@ class OpenPGPScheme(EncryptionScheme): d.addCallback(put_key, openpgp_privkey) return d - def put_key(self, key, address): + def put_key(self, key): """ Put C{key} in local storage. :param key: The key to be stored. :type key: OpenPGPKey - :param address: address for which this key will be active. - :type address: str :return: A Deferred which fires when the key is in the storage. :rtype: Deferred """ - d = self._put_key_doc(key) - d.addCallback(lambda _: self._put_active_doc(key, address)) - return d + def merge_and_put((keydoc, activedoc)): + if not keydoc: + return put_new_key(activedoc) - def _put_key_doc(self, key): - """ - Put key document in soledad + active_content = None + if activedoc: + active_content = activedoc.content + oldkey = build_key_from_dict(OpenPGPKey, keydoc.content, + active_content) - :type key: OpenPGPKey - :rtype: Deferred - """ - def check_and_put(docs, key): - 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( - "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]) + key.merge(oldkey) + keydoc.set_json(key.get_json()) + d = self._soledad.put_doc(keydoc) + d.addCallback(put_active, activedoc) return d - d = self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, - self.KEY_TYPE, - key.key_id, - '1' if key.private else '0') - d.addCallback(check_and_put, key) - return d - - def _put_active_doc(self, key, address): - """ - Put active key document in soledad + def put_new_key(activedoc): + deferreds = [] + if activedoc: + d = self._soledad.delete_doc(activedoc) + deferreds.append(d) + for json in [key.get_json(), key.get_active_json()]: + d = self._soledad.create_doc_from_json(json) + deferreds.append(d) + return defer.gatherResults(deferreds) - :type key: OpenPGPKey - :type addresses: str - :rtype: Deferred - """ - def check_and_put(docs): - if len(docs) == 1: - doc = docs.pop() - doc.set_json(key.get_active_json(address)) - d = self._soledad.put_doc(doc) + def put_active(_, activedoc): + active_json = key.get_active_json() + if activedoc: + activedoc.set_json(active_json) + d = self._soledad.put_doc(activedoc) else: - if len(docs) > 1: - logger.error("There is more than one active key document " - "for the address %s" % (address,)) - deferreds = [] - for doc in docs: - delete = self._soledad.delete_doc(doc) - deferreds.append(delete) - d = defer.gatherResults(deferreds, consumeErrors=True) - else: - d = defer.succeed(None) - - d.addCallback( - lambda _: self._soledad.create_doc_from_json( - key.get_active_json(address))) + d = self._soledad.create_doc_from_json(active_json) return d - d = self._soledad.get_from_index( - TYPE_ADDRESS_PRIVATE_INDEX, - self.ACTIVE_TYPE, - address, - '1' if key.private else '0') - d.addCallback(check_and_put) + def get_active_doc(keydoc): + d = self._get_active_doc_from_address(key.address, key.private) + d.addCallback(lambda activedoc: (keydoc, activedoc)) + return d + + d = self._get_key_doc_from_fingerprint(key.fingerprint, key.private) + d.addCallback(get_active_doc) + d.addCallback(merge_and_put) return d def _get_key_doc(self, address, private=False): @@ -520,48 +518,30 @@ class OpenPGPScheme(EncryptionScheme): :param private: Whether to look for a private key. :type private: bool - :return: A Deferred which fires with the SoledadDocument with the key - or None if it does not exist. + :return: A Deferred which fires with a touple of two SoledadDocument + (keydoc, activedoc) or None if it does not exist. :rtype: Deferred """ def get_key_from_active_doc(activedoc): - if len(activedoc) is 0: - return None - leap_assert( - len(activedoc) is 1, - 'Found more than one key for address %s!' % (address,)) - - key_id = activedoc[0].content[KEY_ID_KEY] - d = self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, - self.KEY_TYPE, - key_id, - '1' if private else '0') - d.addCallback(get_doc, key_id, activedoc) + if not activedoc: + return (None, None) + fingerprint = activedoc.content[KEY_FINGERPRINT_KEY] + d = self._get_key_doc_from_fingerprint(fingerprint, private) + d.addCallback(delete_active_if_no_key, activedoc) return d - 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)) + def delete_active_if_no_key(keydoc, activedoc): + if not keydoc: d = self._soledad.delete_doc(activedoc) - d.addCallback(lambda _: None) + d.addCallback(lambda _: (None, 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] + return (keydoc, activedoc) - d = self._soledad.get_from_index( - TYPE_ADDRESS_PRIVATE_INDEX, - self.ACTIVE_TYPE, - address, - '1' if private else '0') + d = self._get_active_doc_from_address(address, private) d.addCallback(get_key_from_active_doc) return d - def _build_key_from_gpg(self, key, key_data): + def _build_key_from_gpg(self, key, key_data, address=None): """ Build an OpenPGPKey for C{address} based on C{key} from local gpg storage. @@ -569,6 +549,8 @@ class OpenPGPScheme(EncryptionScheme): ASCII armored GPG key data has to be queried independently in this wrapper, so we receive it in C{key_data}. + :param address: Active address for the key. + :type address: str :param key: Key obtained from GPG storage. :type key: dict :param key_data: Key data obtained from GPG storage. @@ -576,7 +558,7 @@ class OpenPGPScheme(EncryptionScheme): :return: An instance of the key. :rtype: OpenPGPKey """ - return build_gpg_key(key, key_data, self._gpgbinary) + return build_gpg_key(key, key_data, address, self._gpgbinary) def delete_key(self, key): """ @@ -601,17 +583,17 @@ class OpenPGPScheme(EncryptionScheme): def get_key_docs(_): return self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, + TYPE_FINGERPRINT_PRIVATE_INDEX, self.KEY_TYPE, - key.key_id, + key.fingerprint, '1' if key.private else '0') def delete_key(docs): if len(docs) == 0: raise errors.KeyNotFound(key) elif len(docs) > 1: - logger.warning("There is more than one key for key_id %s" - % key.key_id) + logger.warning("There is more than one key for fingerprint %s" + % key.fingerprint) has_deleted = False deferreds = [] @@ -625,45 +607,15 @@ class OpenPGPScheme(EncryptionScheme): return defer.gatherResults(deferreds) d = self._soledad.get_from_index( - TYPE_ID_PRIVATE_INDEX, + TYPE_FINGERPRINT_PRIVATE_INDEX, self.ACTIVE_TYPE, - key.key_id, + key.fingerprint, '1' if key.private else '0') d.addCallback(delete_docs) d.addCallback(get_key_docs) d.addCallback(delete_key) return d - def _repair_key_docs(self, doclist, key_id): - """ - If there is more than one key for a key id try to self-repair it - - :return: a Deferred that will be fired once all the deletions are - completed - :rtype: Deferred - """ - logger.error("BUG ---------------------------------------------------") - logger.error("There is more than one key with the same key_id %s:" - % (key_id,)) - - def log_key_doc(doc): - logger.error("\t%s: %s" % (doc.content[KEY_ADDRESS_KEY], - doc.content[KEY_FINGERPRINT_KEY])) - - 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 # @@ -686,6 +638,7 @@ class OpenPGPScheme(EncryptionScheme): raise errors.GPGError( 'Failed to encrypt/decrypt: %s' % stderr) + @defer.inlineCallbacks def encrypt(self, data, pubkey, passphrase=None, sign=None, cipher_algo='AES256'): """ @@ -700,8 +653,8 @@ class OpenPGPScheme(EncryptionScheme): :param cipher_algo: The cipher algorithm to use. :type cipher_algo: str - :return: The encrypted data. - :rtype: str + :return: A Deferred that will be fired with the encrypted data. + :rtype: defer.Deferred :raise EncryptError: Raised if failed encrypting for some reason. """ @@ -713,9 +666,10 @@ class OpenPGPScheme(EncryptionScheme): leap_assert(sign.private is True) keys.append(sign) with TempGPGWrapper(keys, self._gpgbinary) as gpg: - result = gpg.encrypt( + result = yield from_thread( + gpg.encrypt, data, pubkey.fingerprint, - default_key=sign.key_id if sign else None, + default_key=sign.fingerprint if sign else None, passphrase=passphrase, symmetric=False, cipher_algo=cipher_algo) # Here we cannot assert for correctness of sig because the sig is @@ -724,11 +678,12 @@ class OpenPGPScheme(EncryptionScheme): # result.data - (bool) contains the result of the operation try: self._assert_gpg_result_ok(result) - return result.data + defer.returnValue(result.data) except errors.GPGError as e: - logger.error('Failed to decrypt: %s.' % str(e)) + logger.warning('Failed to encrypt: %s.' % str(e)) raise errors.EncryptError() + @defer.inlineCallbacks def decrypt(self, data, privkey, passphrase=None, verify=None): """ Decrypt C{data} using private @{privkey} and verify with C{verify} key. @@ -743,8 +698,9 @@ class OpenPGPScheme(EncryptionScheme): :param verify: The key used to verify a signature. :type verify: OpenPGPKey - :return: The decrypted data and if signature verifies - :rtype: (unicode, bool) + :return: Deferred that will fire with the decrypted data and + if signature verifies (unicode, bool) + :rtype: Deferred :raise DecryptError: Raised if failed decrypting for some reason. """ @@ -756,8 +712,9 @@ class OpenPGPScheme(EncryptionScheme): keys.append(verify) with TempGPGWrapper(keys, self._gpgbinary) as gpg: try: - result = gpg.decrypt( - data, passphrase=passphrase, always_trust=True) + result = yield from_thread(gpg.decrypt, + data, passphrase=passphrase, + always_trust=True) self._assert_gpg_result_ok(result) # verify signature @@ -767,9 +724,9 @@ class OpenPGPScheme(EncryptionScheme): verify.fingerprint == result.pubkey_fingerprint): sign_valid = True - return (result.data, sign_valid) + defer.returnValue((result.data, sign_valid)) except errors.GPGError as e: - logger.error('Failed to decrypt: %s.' % str(e)) + logger.warning('Failed to decrypt: %s.' % str(e)) raise errors.DecryptError(str(e)) def is_encrypted(self, data): @@ -814,7 +771,7 @@ class OpenPGPScheme(EncryptionScheme): # result.fingerprint - contains the fingerprint of the key used to # sign. with TempGPGWrapper(privkey, self._gpgbinary) as gpg: - result = gpg.sign(data, default_key=privkey.key_id, + result = gpg.sign(data, default_key=privkey.fingerprint, digest_algo=digest_algo, clearsign=clearsign, detach=detach, binary=binary) rfprint = privkey.fingerprint @@ -823,7 +780,7 @@ class OpenPGPScheme(EncryptionScheme): if result.fingerprint is None: raise errors.SignFailed( 'Failed to sign with key %s: %s' % - (privkey['keyid'], result.stderr)) + (privkey['fingerprint'], result.stderr)) leap_assert( result.fingerprint == kfprint, 'Signature and private key fingerprints mismatch: ' @@ -867,6 +824,31 @@ class OpenPGPScheme(EncryptionScheme): kfprint = gpgpubkey['fingerprint'] return valid and rfprint == kfprint + def _get_active_doc_from_address(self, address, private): + d = self._soledad.get_from_index( + TYPE_ADDRESS_PRIVATE_INDEX, + self.ACTIVE_TYPE, + address, + '1' if private else '0') + d.addCallback(self._repair_and_get_doc, self._repair_active_docs) + return d + + def _get_key_doc_from_fingerprint(self, fingerprint, private): + d = self._soledad.get_from_index( + TYPE_FINGERPRINT_PRIVATE_INDEX, + self.KEY_TYPE, + fingerprint, + '1' if private else '0') + d.addCallback(self._repair_and_get_doc, self._repair_key_docs) + return d + + def _repair_and_get_doc(self, doclist, repair_func): + if len(doclist) is 0: + return None + elif len(doclist) > 1: + return repair_func(doclist) + return doclist[0] + def process_ascii_key(key_data, gpgbinary, secret=False): with TempGPGWrapper(gpgbinary=gpgbinary) as gpg: @@ -880,18 +862,21 @@ def process_ascii_key(key_data, gpgbinary, secret=False): return info, key -def build_gpg_key(key_info, key_data, gpgbinary=None): +def build_gpg_key(key_info, key_data, address=None, gpgbinary=None): expiry_date = None if key_info['expires']: expiry_date = datetime.fromtimestamp(int(key_info['expires'])) - address = [] + uids = [] for uid in key_info['uids']: - address.append(_parse_address(uid)) + uids.append(_parse_address(uid)) + if address and address not in uids: + raise errors.KeyAddressMismatch("UIDs %s found, but expected %s" + % (str(uids), address)) return OpenPGPKey( - address, + address=address, + uids=uids, gpgbinary=gpgbinary, - key_id=key_info['keyid'], fingerprint=key_info['fingerprint'], key_data=key_data, private=True if key_info['type'] == 'sec' else False, diff --git a/src/leap/keymanager/tests/__init__.py b/src/leap/keymanager/tests/__init__.py index cd612c4..20d05e8 100644 --- a/src/leap/keymanager/tests/__init__.py +++ b/src/leap/keymanager/tests/__init__.py @@ -54,6 +54,14 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): def tearDown(self): km = self._key_manager() + # wait for the indexes to be ready for the tear down + d = km._wrapper_map[OpenPGPKey].deferred_init + d.addCallback(lambda _: self.delete_all_keys(km)) + d.addCallback(lambda _: self.tearDownEnv()) + d.addCallback(lambda _: self._soledad.close()) + return d + + def delete_all_keys(self, km): def delete_keys(keys): deferreds = [] for key in keys: @@ -61,26 +69,18 @@ class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): deferreds.append(d) return gatherResults(deferreds) - def get_and_delete_keys(_): - deferreds = [] - 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 + deferreds = [] + 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 _key_manager(self, user=ADDRESS, url='', token=None, ca_cert_path=None): @@ -97,7 +97,6 @@ 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 856d6da..6347d56 100644 --- a/src/leap/keymanager/tests/test_keymanager.py +++ b/src/leap/keymanager/tests/test_keymanager.py @@ -21,11 +21,14 @@ Tests for the Key Manager. """ from os import path +import json +import urllib from datetime import datetime import tempfile +import pkg_resources from leap.common import ca_bundle from mock import Mock, MagicMock, patch -from twisted.internet.defer import inlineCallbacks +from twisted.internet import defer from twisted.trial import unittest from leap.keymanager import ( @@ -73,25 +76,25 @@ class KeyManagerUtilTestCase(unittest.TestCase): def test_build_key_from_dict(self): kdict = { - 'address': [ADDRESS], - 'key_id': KEY_FINGERPRINT[-16:], + 'uids': [ADDRESS], 'fingerprint': KEY_FINGERPRINT, 'key_data': PUBLIC_KEY, 'private': False, 'length': 4096, 'expiry_date': 0, - 'last_audited_at': 0, 'refreshed_at': 1311239602, + } + adict = { + 'address': ADDRESS, + 'private': False, + 'last_audited_at': 0, 'validation': str(ValidationLevels.Weak_Chain), 'encr_used': False, 'sign_used': True, } - key = build_key_from_dict(OpenPGPKey, kdict) - self.assertEqual( - kdict['address'], key.address, - 'Wrong data in key.') + key = build_key_from_dict(OpenPGPKey, kdict, adict) self.assertEqual( - kdict['key_id'], key.key_id, + kdict['uids'], key.uids, 'Wrong data in key.') self.assertEqual( kdict['fingerprint'], key.fingerprint, @@ -115,34 +118,37 @@ class KeyManagerUtilTestCase(unittest.TestCase): datetime.fromtimestamp(kdict['refreshed_at']), key.refreshed_at, 'Wrong data in key.') self.assertEqual( - ValidationLevels.get(kdict['validation']), key.validation, + adict['address'], key.address, 'Wrong data in key.') self.assertEqual( - kdict['encr_used'], key.encr_used, + ValidationLevels.get(adict['validation']), key.validation, 'Wrong data in key.') self.assertEqual( - kdict['sign_used'], key.sign_used, + adict['encr_used'], key.encr_used, + 'Wrong data in key.') + self.assertEqual( + adict['sign_used'], key.sign_used, 'Wrong data in key.') class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): - @inlineCallbacks + @defer.inlineCallbacks def test_get_all_keys_in_db(self): km = self._key_manager() yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) # get public keys keys = yield km.get_all_keys(False) self.assertEqual(len(keys), 1, 'Wrong number of keys') - self.assertTrue(ADDRESS in keys[0].address) + self.assertTrue(ADDRESS in keys[0].uids) self.assertFalse(keys[0].private) # get private keys keys = yield km.get_all_keys(True) self.assertEqual(len(keys), 1, 'Wrong number of keys') - self.assertTrue(ADDRESS in keys[0].address) + self.assertTrue(ADDRESS in keys[0].uids) self.assertTrue(keys[0].private) - @inlineCallbacks + @defer.inlineCallbacks def test_get_public_key(self): km = self._key_manager() yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) @@ -150,12 +156,12 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield km.get_key(ADDRESS, OpenPGPKey, private=False, fetch_remote=False) self.assertTrue(key is not None) - self.assertTrue(ADDRESS in key.address) + self.assertTrue(ADDRESS in key.uids) self.assertEqual( key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertFalse(key.private) - @inlineCallbacks + @defer.inlineCallbacks def test_get_private_key(self): km = self._key_manager() yield km._wrapper_map[OpenPGPKey].put_ascii_key(PRIVATE_KEY, ADDRESS) @@ -163,7 +169,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield km.get_key(ADDRESS, OpenPGPKey, private=True, fetch_remote=False) self.assertTrue(key is not None) - self.assertTrue(ADDRESS in key.address) + self.assertTrue(ADDRESS in key.uids) self.assertEqual( key.fingerprint.lower(), KEY_FINGERPRINT.lower()) self.assertTrue(key.private) @@ -173,7 +179,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): d = km.send_key(OpenPGPKey) return self.assertFailure(d, KeyNotFound) - @inlineCallbacks + @defer.inlineCallbacks def test_send_key(self): """ Test that request is well formed when sending keys to server. @@ -181,7 +187,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): token = "mytoken" km = self._key_manager(token=token) yield km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY, ADDRESS) - km._fetcher.put = Mock() + km._async_client_pinned.request = Mock(return_value=defer.succeed('')) # the following data will be used on the send km.ca_cert_path = 'capath' km.session_id = 'sessionid' @@ -191,13 +197,15 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): yield km.send_key(OpenPGPKey) # setup expected args pubkey = yield km.get_key(km._address, OpenPGPKey) - data = { + data = urllib.urlencode({ km.PUBKEY_KEY: pubkey.key_data, - } + }) + headers = {'Authorization': [str('Token token=%s' % token)]} + headers['Content-Type'] = ['application/x-www-form-urlencoded'] url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid') - km._fetcher.put.assert_called_once_with( - url, data=data, verify='capath', - headers={'Authorization': 'Token token=%s' % token}, + km._async_client_pinned.request.assert_called_once_with( + str(url), 'PUT', body=str(data), + headers=headers ) def test_fetch_keys_from_server(self): @@ -205,19 +213,19 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): Test that the request is well formed when fetching keys from server. """ km = self._key_manager(url=NICKSERVER_URI) + expected_url = NICKSERVER_URI + '?address=' + ADDRESS_2 def verify_the_call(_): - km._fetcher.get.assert_called_once_with( - NICKSERVER_URI, - data={'address': ADDRESS_2}, - verify='cacertpath', + km._async_client_pinned.request.assert_called_once_with( + expected_url, + 'GET', ) d = self._fetch_key(km, ADDRESS_2, PUBLIC_KEY_2) d.addCallback(verify_the_call) return d - @inlineCallbacks + @defer.inlineCallbacks def test_get_key_fetches_from_server(self): """ Test that getting a key successfuly fetches from server. @@ -226,10 +234,10 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield self._fetch_key(km, ADDRESS, PUBLIC_KEY) self.assertIsInstance(key, OpenPGPKey) - self.assertTrue(ADDRESS in key.address) + self.assertTrue(ADDRESS in key.uids) self.assertEqual(key.validation, ValidationLevels.Provider_Trust) - @inlineCallbacks + @defer.inlineCallbacks def test_get_key_fetches_other_domain(self): """ Test that getting a key successfuly fetches from server. @@ -238,25 +246,18 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): key = yield self._fetch_key(km, ADDRESS_OTHER, PUBLIC_KEY_OTHER) self.assertIsInstance(key, OpenPGPKey) - self.assertTrue(ADDRESS_OTHER in key.address) + self.assertTrue(ADDRESS_OTHER in key.uids) self.assertEqual(key.validation, ValidationLevels.Weak_Chain) def _fetch_key(self, km, address, key): """ :returns: a Deferred that will fire with the OpenPGPKey """ - class Response(object): - status_code = 200 - headers = {'content-type': 'application/json'} - - def json(self): - return {'address': address, 'openpgp': key} - - def raise_for_status(self): - pass + data = json.dumps({'address': address, 'openpgp': key}) # mock the fetcher so it returns the key for ADDRESS_2 - km._fetcher.get = Mock(return_value=Response()) + km._async_client_pinned.request = Mock( + return_value=defer.succeed(data)) km.ca_cert_path = 'cacertpath' # try to key get without fetching from server d_fail = km.get_key(address, OpenPGPKey, fetch_remote=False) @@ -265,7 +266,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): d.addCallback(lambda _: km.get_key(address, OpenPGPKey)) return d - @inlineCallbacks + @defer.inlineCallbacks def test_put_key_ascii(self): """ Test that putting ascii key works @@ -275,9 +276,9 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): yield km.put_raw_key(PUBLIC_KEY, OpenPGPKey, ADDRESS) key = yield km.get_key(ADDRESS, OpenPGPKey) self.assertIsInstance(key, OpenPGPKey) - self.assertTrue(ADDRESS in key.address) + self.assertTrue(ADDRESS in key.uids) - @inlineCallbacks + @defer.inlineCallbacks def test_fetch_uri_ascii_key(self): """ Test that fetch key downloads the ascii key and gets included in @@ -285,11 +286,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ km = self._key_manager() - class Response(object): - ok = True - content = PUBLIC_KEY - - km._fetcher.get = Mock(return_value=Response()) + km._async_client.request = Mock(return_value=defer.succeed(PUBLIC_KEY)) yield km.fetch_key(ADDRESS, "http://site.domain/key", OpenPGPKey) key = yield km.get_key(ADDRESS, OpenPGPKey) @@ -316,25 +313,16 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): """ km = self._key_manager() - class Response(object): - ok = True - content = PUBLIC_KEY - - km._fetcher.get = Mock(return_value=Response()) + km._async_client.request = Mock(return_value=defer.succeed(PUBLIC_KEY)) 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 + km._async_client.request = MagicMock(return_value=defer.succeed(body)) - return mock + return km._async_client.request - @inlineCallbacks + @defer.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) @@ -342,10 +330,9 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): 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()) + get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') - @inlineCallbacks + @defer.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) @@ -353,10 +340,9 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): 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()) + get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') - @inlineCallbacks + @defer.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) @@ -364,14 +350,14 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): 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()) + get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') - @inlineCallbacks + @defer.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_content = pkg_resources.resource_string('leap.common.testing', + 'cacert.pem') ca_cert_path = tmp_input.name self._dump_to_file(ca_cert_path, ca_content) @@ -383,8 +369,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): 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) + get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') # assert that files got appended expected = self._slurp_file(ca_bundle.where()) + ca_content @@ -402,7 +387,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): content = f.read() return content - @inlineCallbacks + @defer.inlineCallbacks def test_decrypt_updates_sign_used_for_signer(self): # given km = self._key_manager() @@ -420,7 +405,7 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase): # then self.assertEqual(True, key.sign_used) - @inlineCallbacks + @defer.inlineCallbacks def test_decrypt_does_not_update_sign_used_for_recipient(self): # given km = self._key_manager() @@ -445,7 +430,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): RAW_DATA = 'data' - @inlineCallbacks + @defer.inlineCallbacks def test_keymanager_openpgp_encrypt_decrypt(self): km = self._key_manager() # put raw private key @@ -464,7 +449,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): fetch_remote=False) self.assertEqual(signingkey.fingerprint, key.fingerprint) - @inlineCallbacks + @defer.inlineCallbacks def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self): km = self._key_manager() # put raw keys @@ -481,7 +466,7 @@ class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): self.assertEqual(self.RAW_DATA, rawdata) self.assertTrue(isinstance(signingkey, errors.InvalidSignature)) - @inlineCallbacks + @defer.inlineCallbacks def test_keymanager_openpgp_sign_verify(self): km = self._key_manager() # put raw private keys diff --git a/src/leap/keymanager/tests/test_migrator.py b/src/leap/keymanager/tests/test_migrator.py new file mode 100644 index 0000000..2d9782b --- /dev/null +++ b/src/leap/keymanager/tests/test_migrator.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# test_migrator.py +# Copyright (C) 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Tests for the migrator. +""" + + +from collections import namedtuple +from mock import Mock +from twisted.internet.defer import succeed, inlineCallbacks + +from leap.keymanager.migrator import KeyDocumentsMigrator, KEY_ID_KEY +from leap.keymanager.keys import ( + TAGS_PRIVATE_INDEX, + KEYMANAGER_ACTIVE_TAG, + KEYMANAGER_KEY_TAG, + KEYMANAGER_DOC_VERSION, + + KEY_ADDRESS_KEY, + KEY_UIDS_KEY, + KEY_VERSION_KEY, + KEY_FINGERPRINT_KEY, + KEY_VALIDATION_KEY, + KEY_LAST_AUDITED_AT_KEY, + KEY_ENCR_USED_KEY, + KEY_SIGN_USED_KEY, +) +from leap.keymanager.validation import ValidationLevels +from leap.keymanager.tests import ( + KeyManagerWithSoledadTestCase, + ADDRESS, + ADDRESS_2, + KEY_FINGERPRINT, +) + + +class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): + @inlineCallbacks + def test_simple_migration(self): + get_from_index = self._soledad.get_from_index + delete_doc = self._soledad.delete_doc + put_doc = self._soledad.put_doc + + def my_get_from_index(*args): + docs = [] + if (args[0] == TAGS_PRIVATE_INDEX and + args[2] == '0'): + SoledadDocument = namedtuple("SoledadDocument", ["content"]) + if args[1] == KEYMANAGER_KEY_TAG: + docs = [SoledadDocument({ + KEY_ADDRESS_KEY: [ADDRESS], + KEY_ID_KEY: KEY_FINGERPRINT[-16:], + KEY_FINGERPRINT_KEY: KEY_FINGERPRINT, + KEY_VALIDATION_KEY: str(ValidationLevels.Weak_Chain), + KEY_LAST_AUDITED_AT_KEY: 0, + KEY_ENCR_USED_KEY: True, + KEY_SIGN_USED_KEY: False, + })] + if args[1] == KEYMANAGER_ACTIVE_TAG: + docs = [SoledadDocument({ + KEY_ID_KEY: KEY_FINGERPRINT[-16:], + })] + return succeed(docs) + + self._soledad.get_from_index = my_get_from_index + self._soledad.delete_doc = Mock(return_value=succeed(None)) + self._soledad.put_doc = Mock(return_value=succeed(None)) + + try: + migrator = KeyDocumentsMigrator(self._soledad) + yield migrator.migrate() + call_list = self._soledad.put_doc.call_args_list + finally: + self._soledad.get_from_index = get_from_index + self._soledad.delete_doc = delete_doc + self._soledad.put_doc = put_doc + + self.assertEqual(len(call_list), 2) + active = call_list[0][0][0] + key = call_list[1][0][0] + + self.assertTrue(KEY_ID_KEY not in active.content) + self.assertEqual(active.content[KEY_VERSION_KEY], + KEYMANAGER_DOC_VERSION) + self.assertEqual(active.content[KEY_FINGERPRINT_KEY], KEY_FINGERPRINT) + self.assertEqual(active.content[KEY_VALIDATION_KEY], + str(ValidationLevels.Weak_Chain)) + self.assertEqual(active.content[KEY_LAST_AUDITED_AT_KEY], 0) + self.assertEqual(active.content[KEY_ENCR_USED_KEY], True) + self.assertEqual(active.content[KEY_SIGN_USED_KEY], False) + + self.assertTrue(KEY_ID_KEY not in key.content) + self.assertTrue(KEY_ADDRESS_KEY not in key.content) + self.assertTrue(KEY_VALIDATION_KEY not in key.content) + self.assertTrue(KEY_LAST_AUDITED_AT_KEY not in key.content) + self.assertTrue(KEY_ENCR_USED_KEY not in key.content) + self.assertTrue(KEY_SIGN_USED_KEY not in key.content) + self.assertEqual(key.content[KEY_UIDS_KEY], [ADDRESS]) + + @inlineCallbacks + def test_two_active_docs(self): + get_from_index = self._soledad.get_from_index + delete_doc = self._soledad.delete_doc + put_doc = self._soledad.put_doc + + def my_get_from_index(*args): + docs = [] + if (args[0] == TAGS_PRIVATE_INDEX and + args[2] == '0'): + SoledadDocument = namedtuple("SoledadDocument", ["content"]) + if args[1] == KEYMANAGER_KEY_TAG: + validation = str(ValidationLevels.Provider_Trust) + docs = [SoledadDocument({ + KEY_ADDRESS_KEY: [ADDRESS, ADDRESS_2], + KEY_ID_KEY: KEY_FINGERPRINT[-16:], + KEY_FINGERPRINT_KEY: KEY_FINGERPRINT, + KEY_VALIDATION_KEY: validation, + KEY_LAST_AUDITED_AT_KEY: 1984, + KEY_ENCR_USED_KEY: True, + KEY_SIGN_USED_KEY: False, + })] + if args[1] == KEYMANAGER_ACTIVE_TAG: + docs = [ + SoledadDocument({ + KEY_ADDRESS_KEY: ADDRESS, + KEY_ID_KEY: KEY_FINGERPRINT[-16:], + }), + SoledadDocument({ + KEY_ADDRESS_KEY: ADDRESS_2, + KEY_ID_KEY: KEY_FINGERPRINT[-16:], + }), + ] + return succeed(docs) + + self._soledad.get_from_index = my_get_from_index + self._soledad.delete_doc = Mock(return_value=succeed(None)) + self._soledad.put_doc = Mock(return_value=succeed(None)) + + try: + migrator = KeyDocumentsMigrator(self._soledad) + yield migrator.migrate() + call_list = self._soledad.put_doc.call_args_list + finally: + self._soledad.get_from_index = get_from_index + self._soledad.delete_doc = delete_doc + self._soledad.put_doc = put_doc + + self.assertEqual(len(call_list), 3) + for active in [call[0][0] for call in call_list][:2]: + self.assertTrue(KEY_ID_KEY not in active.content) + self.assertEqual(active.content[KEY_VERSION_KEY], + KEYMANAGER_DOC_VERSION) + self.assertEqual(active.content[KEY_FINGERPRINT_KEY], + KEY_FINGERPRINT) + self.assertEqual(active.content[KEY_VALIDATION_KEY], + str(ValidationLevels.Weak_Chain)) + self.assertEqual(active.content[KEY_LAST_AUDITED_AT_KEY], 0) + self.assertEqual(active.content[KEY_ENCR_USED_KEY], False) + self.assertEqual(active.content[KEY_SIGN_USED_KEY], False) diff --git a/src/leap/keymanager/tests/test_openpgp.py b/src/leap/keymanager/tests/test_openpgp.py index bae83db..0e5f6be 100644 --- a/src/leap/keymanager/tests/test_openpgp.py +++ b/src/leap/keymanager/tests/test_openpgp.py @@ -29,7 +29,10 @@ from leap.keymanager import ( KeyNotFound, openpgp, ) -from leap.keymanager.keys import TYPE_ID_PRIVATE_INDEX +from leap.keymanager.keys import ( + TYPE_FINGERPRINT_PRIVATE_INDEX, + TYPE_ADDRESS_PRIVATE_INDEX, +) from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.tests import ( KeyManagerWithSoledadTestCase, @@ -37,7 +40,6 @@ from leap.keymanager.tests import ( ADDRESS_2, KEY_FINGERPRINT, PUBLIC_KEY, - KEY_ID, PUBLIC_KEY_2, PRIVATE_KEY, PRIVATE_KEY_2, @@ -109,7 +111,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): # encrypt yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) pubkey = yield pgp.get_key(ADDRESS, private=False) - cyphertext = pgp.encrypt(data, pubkey) + cyphertext = yield pgp.encrypt(data, pubkey) self.assertTrue(cyphertext is not None) self.assertTrue(cyphertext != '') @@ -121,7 +123,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): yield self._assert_key_not_found(pgp, ADDRESS, private=True) yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) - decrypted, _ = pgp.decrypt(cyphertext, privkey) + decrypted, _ = yield pgp.decrypt(cyphertext, privkey) self.assertEqual(decrypted, data) yield pgp.delete_key(pubkey) @@ -171,9 +173,9 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) - self.assertRaises( - AssertionError, - pgp.encrypt, data, privkey, sign=pubkey) + self.failureResultOf( + pgp.encrypt(data, privkey, sign=pubkey), + AssertionError) @inlineCallbacks def test_decrypt_verify_with_private_raises(self): @@ -183,12 +185,11 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = pgp.encrypt( + encrypted_and_signed = yield pgp.encrypt( data, pubkey, sign=privkey) - self.assertRaises( - AssertionError, - pgp.decrypt, - encrypted_and_signed, privkey, verify=privkey) + self.failureResultOf( + pgp.decrypt(encrypted_and_signed, privkey, verify=privkey), + AssertionError) @inlineCallbacks def test_decrypt_verify_with_wrong_key(self): @@ -198,11 +199,12 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): yield pgp.put_ascii_key(PRIVATE_KEY, ADDRESS) privkey = yield pgp.get_key(ADDRESS, private=True) pubkey = yield pgp.get_key(ADDRESS, private=False) - encrypted_and_signed = pgp.encrypt(data, pubkey, sign=privkey) + encrypted_and_signed = yield pgp.encrypt(data, pubkey, sign=privkey) yield pgp.put_ascii_key(PUBLIC_KEY_2, ADDRESS_2) wrongkey = yield pgp.get_key(ADDRESS_2) - decrypted, validsign = pgp.decrypt(encrypted_and_signed, privkey, - verify=wrongkey) + decrypted, validsign = yield pgp.decrypt(encrypted_and_signed, + privkey, + verify=wrongkey) self.assertEqual(decrypted, data) self.assertFalse(validsign) @@ -232,9 +234,9 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): privkey2 = yield pgp.get_key(ADDRESS_2, private=True) data = 'data' - encrypted_and_signed = pgp.encrypt( + encrypted_and_signed = yield pgp.encrypt( data, pubkey2, sign=privkey) - res, validsign = pgp.decrypt( + res, validsign = yield pgp.decrypt( encrypted_and_signed, privkey2, verify=pubkey) self.assertEqual(data, res) self.assertTrue(validsign) @@ -253,39 +255,18 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_self_repair_three_keys(self): + refreshed_keep = datetime(2007, 1, 1) + self._insert_key_docs([datetime(2005, 1, 1), + refreshed_keep, + datetime(2001, 1, 1)]) + delete_doc = self._mock_delete_doc() + 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 + self.assertEqual(key.refreshed_at, refreshed_keep) + self.assertEqual(self.count, 2) + self._soledad.delete_doc = delete_doc @inlineCallbacks def test_self_repair_no_keys(self): @@ -297,8 +278,8 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): delete_doc = self._soledad.delete_doc def my_get_from_index(*args): - if (args[0] == TYPE_ID_PRIVATE_INDEX and - args[2] == KEY_ID): + if (args[0] == TYPE_FINGERPRINT_PRIVATE_INDEX and + args[2] == KEY_FINGERPRINT): return succeed([]) return get_from_index(*args) @@ -308,6 +289,7 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): try: yield self.assertFailure(pgp.get_key(ADDRESS, private=False), KeyNotFound) + # it should have deleted the index self.assertEqual(self._soledad.delete_doc.call_count, 1) finally: self._soledad.get_from_index = get_from_index @@ -315,6 +297,19 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): @inlineCallbacks def test_self_repair_put_keys(self): + self._insert_key_docs([datetime(2005, 1, 1), + datetime(2007, 1, 1), + datetime(2001, 1, 1)]) + delete_doc = self._mock_delete_doc() + + pgp = openpgp.OpenPGPScheme( + self._soledad, gpgbinary=self.gpg_binary_path) + yield pgp.put_ascii_key(PUBLIC_KEY, ADDRESS) + self.assertEqual(self.count, 2) + self._soledad.delete_doc = delete_doc + + @inlineCallbacks + def test_self_repair_five_active_docs(self): pgp = openpgp.OpenPGPScheme( self._soledad, gpgbinary=self.gpg_binary_path) @@ -322,29 +317,41 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): 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]) + if (args[0] == TYPE_ADDRESS_PRIVATE_INDEX and + args[2] == ADDRESS): + k1 = OpenPGPKey(ADDRESS, fingerprint="1", + last_audited_at=datetime(2005, 1, 1)) + k2 = OpenPGPKey(ADDRESS, fingerprint="2", + last_audited_at=datetime(2007, 1, 1)) + k3 = OpenPGPKey(ADDRESS, fingerprint="3", + last_audited_at=datetime(2007, 1, 1), + encr_used=True, sign_used=True) + k4 = OpenPGPKey(ADDRESS, fingerprint="4", + last_audited_at=datetime(2007, 1, 1), + sign_used=True) + k5 = OpenPGPKey(ADDRESS, fingerprint="5", + last_audited_at=datetime(2007, 1, 1), + encr_used=True) + deferreds = [] + for k in (k1, k2, k3, k4, k5): + d = self._soledad.create_doc_from_json( + k.get_active_json()) + deferreds.append(d) + return gatherResults(deferreds) + elif args[0] == TYPE_FINGERPRINT_PRIVATE_INDEX: + fingerprint = args[2] + self.assertEqual(fingerprint, "3") + k = OpenPGPKey(ADDRESS, fingerprint="3") + return succeed( + [self._soledad.create_doc_from_json(k.get_json())]) 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) + yield pgp.get_key(ADDRESS, private=False) + self.assertEqual(self._soledad.delete_doc.call_count, 4) finally: self._soledad.get_from_index = get_from_index self._soledad.delete_doc = delete_doc @@ -352,3 +359,21 @@ class OpenPGPCryptoTestCase(KeyManagerWithSoledadTestCase): def _assert_key_not_found(self, pgp, address, private=False): d = pgp.get_key(address, private=private) return self.assertFailure(d, KeyNotFound) + + @inlineCallbacks + def _insert_key_docs(self, refreshed_at): + for date in refreshed_at: + key = OpenPGPKey(ADDRESS, fingerprint=KEY_FINGERPRINT, + refreshed_at=date) + yield self._soledad.create_doc_from_json(key.get_json()) + yield self._soledad.create_doc_from_json(key.get_active_json()) + + def _mock_delete_doc(self): + delete_doc = self._soledad.delete_doc + self.count = 0 + + def my_delete_doc(*args): + self.count += 1 + return delete_doc(*args) + self._soledad.delete_doc = my_delete_doc + return delete_doc diff --git a/src/leap/keymanager/tests/test_validation.py b/src/leap/keymanager/tests/test_validation.py index bcf41c4..44d6e39 100644 --- a/src/leap/keymanager/tests/test_validation.py +++ b/src/leap/keymanager/tests/test_validation.py @@ -108,14 +108,14 @@ class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase): TEXT = "some text" km = self._key_manager() + yield km.put_raw_key(UNEXPIRED_PRIVATE, OpenPGPKey, ADDRESS) + signature = yield km.sign(TEXT, ADDRESS, OpenPGPKey) + yield self.delete_all_keys(km) + yield km.put_raw_key(UNEXPIRED_KEY, OpenPGPKey, ADDRESS) yield km.encrypt(TEXT, ADDRESS, OpenPGPKey) - - km2 = self._key_manager() - yield km2.put_raw_key(UNEXPIRED_PRIVATE, OpenPGPKey, ADDRESS) - signature = yield km2.sign(TEXT, ADDRESS, OpenPGPKey) - yield km.verify(TEXT, ADDRESS, OpenPGPKey, detached_sig=signature) + d = km.put_raw_key( UNRELATED_KEY, OpenPGPKey, ADDRESS, validation=ValidationLevels.Provider_Endorsement) @@ -126,17 +126,17 @@ class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase): TEXT = "some text" km = self._key_manager() + yield km.put_raw_key(UNEXPIRED_PRIVATE, OpenPGPKey, ADDRESS) + yield km.put_raw_key(PUBLIC_KEY_2, OpenPGPKey, ADDRESS_2) + encrypted = yield km.encrypt(TEXT, ADDRESS_2, OpenPGPKey, + sign=ADDRESS) + yield self.delete_all_keys(km) + 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) @@ -150,6 +150,33 @@ class ValidationLevelsTestCase(KeyManagerWithSoledadTestCase): key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) self.assertEqual(key.fingerprint, SIGNED_FINGERPRINT) + @inlineCallbacks + def test_two_uuids(self): + TEXT = "some text" + + km = self._key_manager() + yield km.put_raw_key(UUIDS_PRIVATE, OpenPGPKey, ADDRESS_2) + signature = yield km.sign(TEXT, ADDRESS_2, OpenPGPKey) + yield self.delete_all_keys(km) + + yield km.put_raw_key(UUIDS_KEY, OpenPGPKey, ADDRESS_2) + yield km.put_raw_key(UUIDS_KEY, OpenPGPKey, ADDRESS) + yield km.encrypt(TEXT, ADDRESS_2, OpenPGPKey) + yield km.verify(TEXT, ADDRESS_2, OpenPGPKey, detached_sig=signature) + + d = km.put_raw_key( + PUBLIC_KEY_2, OpenPGPKey, ADDRESS_2, + validation=ValidationLevels.Provider_Endorsement) + yield self.assertFailure(d, KeyNotValidUpgrade) + key = yield km.get_key(ADDRESS_2, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, UUIDS_FINGERPRINT) + + yield km.put_raw_key( + PUBLIC_KEY, OpenPGPKey, ADDRESS, + validation=ValidationLevels.Provider_Endorsement) + key = yield km.get_key(ADDRESS, OpenPGPKey, fetch_remote=False) + self.assertEqual(key.fingerprint, KEY_FINGERPRINT) + # Key material for testing @@ -364,6 +391,117 @@ X2+l7IOSt+31KQCBFN/VmhTySJOVQC1d2A56lSH2c/DWVClji+x3suzn -----END PGP PUBLIC KEY BLOCK----- """ +# key 0x1DDBAEB928D982F7: public key two uuids +# uid anotheruser <anotheruser@leap.se> +# uid Leap Test Key <leap@leap.se> +UUIDS_FINGERPRINT = "21690ED054C1B2F3ACE963D38FCC7DEFB4EE5A9B" +UUIDS_KEY = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +vtQLNMD9Gn7D +=MCXv +-----END PGP PUBLIC KEY BLOCK----- +""" +UUIDS_PRIVATE = """ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: GnuPG v1 + +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 +eNWbiM9EvtQLNMD9Gn7D +=/3u/ +-----END PGP PRIVATE KEY BLOCK----- +""" + if __name__ == "__main__": - import unittest unittest.main() diff --git a/src/leap/keymanager/validation.py b/src/leap/keymanager/validation.py index 734cfce..8cf96da 100644 --- a/src/leap/keymanager/validation.py +++ b/src/leap/keymanager/validation.py @@ -118,7 +118,9 @@ def can_upgrade(new_key, old_key): return True # New key signed by the old key - if old_key.key_id in new_key.signatures: + # XXX: signatures are using key-ids instead of fingerprints + key_id = old_key.fingerprint[-16:] + if key_id in new_key.signatures: return True return False |