From b833d9042da3a1650fde3354f38998a2e497672b Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 19 Apr 2013 21:48:57 -0300 Subject: Make keymanager OpenPGP wrapper store using Soledad. --- src/leap/common/keymanager/__init__.py | 29 ++-- src/leap/common/keymanager/errors.py | 1 - src/leap/common/keymanager/gpg.py | 1 - src/leap/common/keymanager/keys.py | 25 ++- src/leap/common/keymanager/openpgp.py | 282 ++++++++++++++++++++++++++----- src/leap/common/tests/test_keymanager.py | 26 ++- 6 files changed, 289 insertions(+), 75 deletions(-) diff --git a/src/leap/common/keymanager/__init__.py b/src/leap/common/keymanager/__init__.py index 8296b92..d197e4c 100644 --- a/src/leap/common/keymanager/__init__.py +++ b/src/leap/common/keymanager/__init__.py @@ -21,12 +21,6 @@ Key Manager is a Nicknym agent for LEAP client. """ -try: - import simplejson as json -except ImportError: - import json # noqa - - from u1db.errors import HTTPError @@ -42,20 +36,22 @@ from leap.common.keymanager.openpgp import ( class KeyManager(object): - def __init__(self, address, url): + def __init__(self, address, url, soledad): """ Initialize a Key Manager for user's C{address} with provider's nickserver reachable in C{url}. @param address: The address of the user of this Key Manager. @type address: str - @param url: The URL of the key manager. + @param url: The URL of the nickserver. @type url: str + @param soledad: A Soledad instance for local storage of keys. + @type soledad: leap.soledad.Soledad """ - self.address = address - self.url = url - self.wrapper_map = { - OpenPGPKey: OpenPGPWrapper(), + self._address = address + self._url = url + self._wrapper_map = { + OpenPGPKey: OpenPGPWrapper(soledad), } def send_key(self, ktype, send_private=False, password=None): @@ -97,16 +93,15 @@ class KeyManager(object): keyserver. """ try: - return self.wrapper_map[ktype].get_key(address) + return self._wrapper_map[ktype].get_key(address) except KeyNotFound: key = filter(lambda k: isinstance(k, ktype), self._fetch_keys(address)) if key is None: raise KeyNotFound() - self.wrapper_map[ktype].put_key(key) + self._wrapper_map[ktype].put_key(key) return key - def _fetch_keys(self, address): """ Fetch keys bound to C{address} from nickserver. @@ -119,11 +114,13 @@ class KeyManager(object): @raise KeyNotFound: If the key was not found on nickserver. @raise httplib.HTTPException: """ + raise NotImplementedError(self._fetch_keys) def refresh_keys(self): """ Update the user's db of validated keys to see if there are changes. """ + raise NotImplementedError(self.refresh_keys) def gen_key(self, ktype): """ @@ -135,4 +132,4 @@ class KeyManager(object): @return: The generated key. @rtype: EncryptionKey """ - return self.wrapper_map[ktype].gen_key(self.address) + return self._wrapper_map[ktype].gen_key(self._address) diff --git a/src/leap/common/keymanager/errors.py b/src/leap/common/keymanager/errors.py index f5bb1ab..4853869 100644 --- a/src/leap/common/keymanager/errors.py +++ b/src/leap/common/keymanager/errors.py @@ -16,7 +16,6 @@ # along with this program. If not, see . - class KeyNotFound(Exception): """ Raised when key was no found on keyserver. diff --git a/src/leap/common/keymanager/gpg.py b/src/leap/common/keymanager/gpg.py index dc5d791..5571ace 100644 --- a/src/leap/common/keymanager/gpg.py +++ b/src/leap/common/keymanager/gpg.py @@ -395,4 +395,3 @@ class GPGWrapper(gnupg.GPG): @rtype: bool """ self.is_encrypted_asym() or self.is_encrypted_sym() - diff --git a/src/leap/common/keymanager/keys.py b/src/leap/common/keymanager/keys.py index 13e3c0b..2e1ed89 100644 --- a/src/leap/common/keymanager/keys.py +++ b/src/leap/common/keymanager/keys.py @@ -21,6 +21,12 @@ Abstact key type and wrapper representations. """ +try: + import simplejson as json +except ImportError: + import json # noqa + + from abc import ABCMeta, abstractmethod @@ -44,13 +50,13 @@ class EncryptionKey(object): __metaclass__ = ABCMeta def __init__(self, address, key_id=None, fingerprint=None, - key_data=None, length=None, expiry_date=None, - validation=None, first_seen_at=None, - last_audited_at=None): + key_data=None, private=None, length=None, expiry_date=None, + validation=None, first_seen_at=None, last_audited_at=None): self.address = address self.key_id = key_id self.fingerprint = fingerprint self.key_data = key_data + self.private = private self.length = length self.expiry_date = expiry_date self.validation = validation @@ -66,10 +72,11 @@ class EncryptionKey(object): """ return json.dumps({ 'address': self.address, - 'type': str(self.__type__), + 'type': str(self.__class__), 'key_id': self.key_id, 'fingerprint': self.fingerprint, 'key_data': self.key_data, + 'private': self.private, 'length': self.length, 'expiry_date': self.expiry_date, 'validation': self.validation, @@ -92,6 +99,15 @@ class KeyTypeWrapper(object): __metaclass__ = ABCMeta + def __init__(self, soledad): + """ + Initialize the Key Type Wrapper. + + @param soledad: A Soledad instance for local storage of keys. + @type soledad: leap.soledad.Soledad + """ + self._soledad = soledad + @abstractmethod def get_key(self, address): """ @@ -124,4 +140,3 @@ class KeyTypeWrapper(object): @return: The key bound to C{address}. @rtype: EncryptionKey """ - diff --git a/src/leap/common/keymanager/openpgp.py b/src/leap/common/keymanager/openpgp.py index bb73089..1c51d94 100644 --- a/src/leap/common/keymanager/openpgp.py +++ b/src/leap/common/keymanager/openpgp.py @@ -22,7 +22,10 @@ Infrastructure for using OpenPGP keys in Key Manager. import re +import tempfile +import shutil +from hashlib import sha256 from leap.common.keymanager.errors import ( KeyNotFound, KeyAlreadyExists, @@ -34,6 +37,153 @@ from leap.common.keymanager.keys import ( from leap.common.keymanager.gpg import GPGWrapper +# +# Utility functions +# + +def _is_address(address): + """ + Return whether the given C{address} is in the form user@provider. + + @param address: The address to be tested. + @type address: str + @return: Whether C{address} is in the form user@provider. + @rtype: bool + """ + return bool(re.match('[\w.-]+@[\w.-]+', address)) + + +def _build_key_from_doc(address, doc): + """ + Build an OpenPGPKey for C{address} based on C{doc} from local storage. + + @param address: The address bound to the key. + @type address: str + @param doc: Document obtained from Soledad storage. + @type doc: leap.soledad.backends.leap_backend.LeapDocument + @return: The built key. + @rtype: OpenPGPKey + """ + return OpenPGPKey( + address, + key_id=doc.content['key_id'], + fingerprint=doc.content['fingerprint'], + key_data=doc.content['key_data'], + private=doc.content['private'], + length=doc.content['length'], + expiry_date=doc.content['expiry_date'], + validation=None, # TODO: verify for validation. + ) + + +def _build_key_from_gpg(address, key, key_data): + """ + Build an OpenPGPKey for C{address} based on C{key} from + local gpg storage. + + ASCII armored GPG key data has to be queried independently in this + wrapper, so we receive it in C{key_data}. + + @param address: The address bound to the key. + @type address: str + @param key: Key obtained from GPG storage. + @type key: dict + @param key_data: Key data obtained from GPG storage. + @type key_data: str + @return: The built key. + @rtype: OpenPGPKey + """ + return OpenPGPKey( + address, + key_id=key['keyid'], + fingerprint=key['fingerprint'], + key_data=key_data, + private=True if key['type'] == 'sec' else False, + length=key['length'], + expiry_date=key['expires'], + validation=None, # TODO: verify for validation. + ) + + +def _keymanager_doc_id(address, private=False): + """ + Return the document id for the document containing a key for + C{address}. + + @param address: The address bound to the key. + @type address: str + @param private: Whether the key is private or not. + @type private: bool + @return: The document id for the document that stores a key bound to + C{address}. + @rtype: str + """ + assert _is_address(address) + ktype = 'private' if private else 'public' + return sha256('key-manager-'+address+'-'+ktype).hexdigest() + + +def _build_unitary_gpgwrapper(key_data=None): + """ + Return a temporary GPG wrapper keyring containing exactly zero or one + keys. + + Temporary unitary keyrings allow the to use GPG's facilities for exactly + one key. This function creates an empty temporary keyring and imports + C{key_data} if it is not None. + + @param key_data: ASCII armored key data. + @type key_data: str + @return: A GPG wrapper with a unitary keyring. + @rtype: gnupg.GPG + """ + tmpdir = tempfile.mkdtemp() + gpg = GPGWrapper(gnupghome=tmpdir) + assert len(gpg.list_keys()) is 0 + if key_data: + gpg.import_keys(key_data) + assert len(gpg.list_keys()) is 1 + return gpg + + +def _destroy_unitary_gpgwrapper(gpg): + """ + Securely erase a unitary keyring. + + @param gpg: A GPG wrapper instance. + @type gpg: gnupg.GPG + """ + for secret in [True, False]: + for key in gpg.list_keys(secret=secret): + gpg.delete_keys( + key['fingerprint'], + secret=secret) + assert len(gpg.list_keys()) == 0 + # TODO: implement some kind of wiping of data or a more secure way that + # does not write to disk. + shutil.rmtree(gpg.gnupghome) + + +def _safe_call(callback, key_data=None, **kwargs): + """ + Run C{callback} in an unitary keyring containing C{key_data}. + + @param callback: Function whose first argument is the gpg keyring. + @type callback: function(gnupg.GPG) + @param key_data: ASCII armored key data. + @type key_data: str + @param **kwargs: Other eventual parameters for the callback. + @type **kwargs: **dict + """ + gpg = _build_unitary_gpgwrapper(key_data) + callback(gpg, **kwargs) + _destroy_unitary_gpgwrapper(gpg) + + +# +# The OpenPGP wrapper +# + class OpenPGPKey(EncryptionKey): """ Base class for OpenPGP keys. @@ -45,33 +195,19 @@ class OpenPGPWrapper(KeyTypeWrapper): A wrapper for OpenPGP keys. """ - def __init__(self, gnupghome=None): - self._gpg = GPGWrapper(gnupghome=gnupghome) - - def _build_key(self, address, result): + def __init__(self, soledad): """ - Build an OpenPGPWrapper key for C{address} based on C{result} from - local storage. + Initialize the OpenPGP wrapper. - @param address: The address bound to the key. - @type address: str - @param result: Result obtained from GPG storage. - @type result: dict + @param soledad: A Soledad instance for key storage. + @type soledad: leap.soledad.Soledad """ - key_data = self._gpg.export_keys(result['fingerprint'], secret=False) - return OpenPGPKey( - address, - key_id=result['keyid'], - fingerprint=result['fingerprint'], - key_data=key_data, - length=result['length'], - expiry_date=result['expires'], - validation=None, # TODO: verify for validation. - ) + KeyTypeWrapper.__init__(self, soledad) + self._soledad = soledad def gen_key(self, address): """ - Generate an OpenPGP keypair for C{address}. + Generate an OpenPGP keypair bound to C{address}. @param address: The address bound to the key. @type address: str @@ -79,21 +215,36 @@ class OpenPGPWrapper(KeyTypeWrapper): @rtype: OpenPGPKey @raise KeyAlreadyExists: If key already exists in local database. """ + # make sure the key does not already exist + assert _is_address(address) try: self.get_key(address) - raise KeyAlreadyExists() + raise KeyAlreadyExists(address) except KeyNotFound: pass - params = self._gpg.gen_key_input( - key_type='RSA', - key_length=4096, - name_real=address, - name_email=address, - name_comment='Generated by LEAP Key Manager.') - self._gpg.gen_key(params) - return self.get_key(address) - - def get_key(self, address): + + def _gen_key_cb(gpg): + params = gpg.gen_key_input( + key_type='RSA', + key_length=4096, + name_real=address, + name_email=address, + name_comment='Generated by LEAP Key Manager.') + gpg.gen_key(params) + assert len(gpg.list_keys()) is 1 # a unitary keyring! + key = gpg.list_keys(secret=True).pop() + assert len(key['uids']) is 1 # with just one uid! + # assert for correct address + assert re.match('.*<%s>$' % address, key['uids'][0]) is not None + openpgp_key = _build_key_from_gpg( + address, key, + gpg.export_keys(key['fingerprint'])) + self.put_key(openpgp_key) + + _safe_call(_gen_key_cb) + return self.get_key(address, private=True) + + def get_key(self, address, private=False): """ Get key bound to C{address} from local storage. @@ -104,23 +255,62 @@ class OpenPGPWrapper(KeyTypeWrapper): @rtype: OpenPGPKey @raise KeyNotFound: If the key was not found on local storage. """ - m = re.compile('.*<%s>$' % address) - keys = self._gpg.list_keys(secret=False) + assert _is_address(address) + doc = self._get_key_doc(address, private) + if doc is None: + raise KeyNotFound(address) + return _build_key_from_doc(address, doc) - def bound_to_address(key): - return bool(filter(lambda u: m.match(u), key['uids'])) + def put_key_raw(self, data): + """ + Put key contained in raw C{data} in local storage. - try: - bound_key = filter(bound_to_address, keys).pop() - return self._build_key(address, bound_key) - except IndexError: - raise KeyNotFound(address) + @param data: The key data to be stored. + @type data: str + """ + assert data is not None + + def _put_key_raw_cb(gpg): + + key = gpg.list_keys(secret=False).pop() # unitary keyring + # extract adress from first uid on key + match = re.match('.*<([\w.-]+@[\w.-]+)>.*', key['uids'].pop()) + assert match is not None + address = match.group(1) + openpgp_key = _build_key_from_gpg( + address, key, + gpg.export_keys(key['fingerprint'])) + self.put_key(openpgp_key) - def put_key(self, data): + _safe_call(_put_key_raw_cb, data) + + def put_key(self, key): + """ + Put C{key} in local storage. + + @param key: The key to be stored. + @type key: OpenPGPKey + """ + doc = self._get_key_doc(key.address, private=key.private) + if doc is None: + self._soledad.create_doc_from_json( + key.get_json(), + doc_id=_keymanager_doc_id(key.address, key.private)) + else: + doc.set_json(key.get_json()) + self._soledad.put_doc(doc) + + def _get_key_doc(self, address, private=False): """ - Put key contained in {data} in local storage. + Get the document with a key (public, by default) bound to C{address}. + + If C{private} is True, looks for a private key instead of a public. - @param key: The key data to be stored. - @type key: str + @param address: The address bound to the key. + @type address: str + @param private: Whether to look for a private key. + @type private: bool + @return: The document with the key or None if it does not exist. + @rtype: leap.soledad.backends.leap_backend.LeapDocument """ - self._gpg.import_keys(data) + return self._soledad.get_doc(_keymanager_doc_id(address, private)) diff --git a/src/leap/common/tests/test_keymanager.py b/src/leap/common/tests/test_keymanager.py index 4189aac..23d702b 100644 --- a/src/leap/common/tests/test_keymanager.py +++ b/src/leap/common/tests/test_keymanager.py @@ -26,12 +26,26 @@ import unittest from leap.common.testing.basetest import BaseLeapTest from leap.common.keymanager import KeyManager, openpgp, KeyNotFound - +from leap.soledad import Soledad +from leap.common.keymanager.gpg import GPGWrapper class KeyManagerTestCase(BaseLeapTest): def setUp(self): - pass + self._soledad = Soledad( + "leap@leap.se", + "123456", + gnupg_home=self.tempdir+"/gnupg", + secret_path=self.tempdir+"/secret.gpg", + local_db_path=self.tempdir+"/soledad.u1db", + bootstrap=False, + ) + # initialize solead by hand for testing purposes + self._soledad._init_dirs() + self._soledad._gpg = GPGWrapper(gnupghome=self.tempdir+"/gnupg") + self._soledad._shared_db = None + self._soledad._init_keys() + self._soledad._init_db() def tearDown(self): pass @@ -40,7 +54,7 @@ class KeyManagerTestCase(BaseLeapTest): return KeyManager(user, url) def test_openpgp_gen_key(self): - pgp = openpgp.OpenPGPWrapper(self.tempdir+'/gnupg') + pgp = openpgp.OpenPGPWrapper(self._soledad) try: pgp.get_key('user@leap.se') except KeyNotFound: @@ -51,12 +65,12 @@ class KeyManagerTestCase(BaseLeapTest): self.assertEqual( '4096', key.length, 'Wrong key length.') - def test_openpgp_put_key(self): - pgp = openpgp.OpenPGPWrapper(self.tempdir+'/gnupg2') + def test_openpgp_put_key_raw(self): + pgp = openpgp.OpenPGPWrapper(self._soledad) try: pgp.get_key('leap@leap.se') except KeyNotFound: - pgp.put_key(PUBLIC_KEY) + pgp.put_key_raw(PUBLIC_KEY) key = pgp.get_key('leap@leap.se') self.assertIsInstance(key, openpgp.OpenPGPKey) self.assertEqual( -- cgit v1.2.3