diff options
| author | drebs <drebs@leap.se> | 2013-04-19 21:48:57 -0300 | 
|---|---|---|
| committer | drebs <drebs@leap.se> | 2013-04-19 21:57:18 -0300 | 
| commit | b833d9042da3a1650fde3354f38998a2e497672b (patch) | |
| tree | 7129bf066d772f228f6431d7a09cbad15b861cf2 /src | |
| parent | 32999ef8d08b6e94d356ea5fbce43ceebbf5247c (diff) | |
Make keymanager OpenPGP wrapper store using Soledad.
Diffstat (limited to 'src')
| -rw-r--r-- | src/leap/common/keymanager/__init__.py | 29 | ||||
| -rw-r--r-- | src/leap/common/keymanager/errors.py | 1 | ||||
| -rw-r--r-- | src/leap/common/keymanager/gpg.py | 1 | ||||
| -rw-r--r-- | src/leap/common/keymanager/keys.py | 25 | ||||
| -rw-r--r-- | src/leap/common/keymanager/openpgp.py | 282 | ||||
| -rw-r--r-- | 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 <http://www.gnu.org/licenses/>. -  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( | 
