Adapt get_key() and send_key() to the spec.
authordrebs <drebs@leap.se>
Mon, 20 May 2013 20:53:45 +0000 (17:53 -0300)
committerdrebs <drebs@leap.se>
Mon, 20 May 2013 20:55:26 +0000 (17:55 -0300)
* Use session_id instead of token for now.
* Receive info needed to interact with webapp API as params and
  setters/getters.
* Use the webapp API for sending the key to server.
* Prevent from refreshing own key.

src/leap/common/keymanager/__init__.py
src/leap/common/keymanager/keys.py
src/leap/common/tests/test_keymanager.py

index 30a9146..ad9bb3b 100644 (file)
@@ -34,6 +34,7 @@ from leap.common.keymanager.errors import (
 )
 from leap.common.keymanager.keys import (
     build_key_from_dict,
+    KEYMANAGER_KEY_TAG,
 )
 from leap.common.keymanager.openpgp import (
     OpenPGPKey,
@@ -42,6 +43,10 @@ from leap.common.keymanager.openpgp import (
 )
 
 
+#
+# key indexing constants.
+#
+
 TAGS_INDEX = 'by-tags'
 TAGS_AND_PRIVATE_INDEX = 'by-tags-and-private'
 INDEXES = {
@@ -50,9 +55,21 @@ INDEXES = {
 }
 
 
+#
+# The Key Manager
+#
+
 class KeyManager(object):
 
-    def __init__(self, address, nickserver_url, soledad, token=None):
+    #
+    # server's key storage constants
+    #
+
+    OPENPGP_KEY = 'openpgp'
+    PUBKEY_KEY = "user[public_key]"
+
+    def __init__(self, address, nickserver_uri, soledad, session_id=None,
+                 ca_cert_path=None, api_uri=None, api_version=None, uid=None):
         """
         Initialize a Key Manager for user's C{address} with provider's
         nickserver reachable in C{url}.
@@ -63,17 +80,35 @@ class KeyManager(object):
         @type url: str
         @param soledad: A Soledad instance for local storage of keys.
         @type soledad: leap.soledad.Soledad
+        @param session_id: The session ID for interacting with the webapp API.
+        @type session_id: str
+        @param ca_cert_path: The path to the CA certificate.
+        @type ca_cert_path: str
+        @param api_uri: The URI of the webapp API.
+        @type api_uri: str
+        @param api_version: The version of the webapp API.
+        @type api_version: str
+        @param uid: The users' UID.
+        @type uid: str
         """
         self._address = address
-        self._nickserver_url = nickserver_url
+        self._nickserver_uri = nickserver_uri
         self._soledad = soledad
-        self.token = token
+        self._session_id = session_id
+        self.ca_cert_path = ca_cert_path
+        self.api_uri = api_uri
+        self.api_version = api_version
+        self.uid = uid
+        # a dict to map key types to their handlers
         self._wrapper_map = {
             OpenPGPKey: OpenPGPScheme(soledad),
             # other types of key will be added to this mapper.
         }
+        # initialize the indexes needed to query the database
         self._init_indexes()
+        # the following are used to perform https requests
         self._fetcher = requests
+        self._session = self._fetcher.session()
 
     #
     # utilities
@@ -107,24 +142,81 @@ class KeyManager(object):
             self._soledad.delete_index(name)
             self._soledad.create_index(name, *expression)
 
-    def _get_dict_from_http_json(self, path):
+    def _get(self, uri, data=None):
         """
-        Make a GET HTTP request and return a dictionary containing the
-        response.
+        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
+
+        @return: The response to the request.
+        @rtype: requests.Response
         """
-        response = self._fetcher.get(self._nickserver_url+path)
-        leap_assert(response.status_code == 200, 'Invalid response.')
         leap_assert(
-            response.headers['content-type'].startswith('application/json')
-            is True,
+            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)
+        # assert that the response is valid
+        res.raise_for_status()
+        leap_assert(
+            res.headers['content-type'].startswith('application/json'),
             'Content-type is not JSON.')
-        return response.json()
+        return res
+
+    def _put(self, uri, data=None):
+        """
+        Send a PUT request to C{uri} containing C{data}.
+
+        The request will be sent using the configured CA certificate path to
+        verify the server certificate and the configured session id for
+        authentication.
+
+        @param uri: The URI of the request.
+        @type uri: str
+        @param data: The body of the request.
+        @type data: dict, str or file
+
+        @return: The response to the request.
+        @rtype: requests.Response
+        """
+        leap_assert(
+            self._ca_cert_path is not None,
+            'We need the CA certificate path!')
+        leap_assert(
+            self._session_id is not None,
+            'We need a session_id to interact with webapp!')
+        res = self._fetcher.put(
+            uri, data=data, verify=self._ca_cert_path,
+            cookies={'_session_id': self._session_id})
+        # assert that the response is valid
+        res.raise_for_status()
+        return res
+
+    def _fetch_keys_from_server(self, address):
+        """
+        Fetch keys bound to C{address} from nickserver and insert them in
+        local database.
+
+        @param address: The address bound to the keys.
+        @type address: str
+
+        @raise KeyNotFound: If the key was not found on nickserver.
+        """
+        # request keys from the nickserver
+        server_keys = self._get(
+            self._nickserver_uri, {'address': address}).json()
+        # insert keys in local database
+        if self.OPENPGP_KEY in server_keys:
+            self._wrapper_map[OpenPGPKey].put_ascii_key(
+                server_keys['openpgp'])
 
     #
     # key management
     #
 
-    def send_key(self, ktype, send_private=False, password=None):
+    def send_key(self, ktype):
         """
         Send user's key of type C{ktype} to provider.
 
@@ -140,33 +232,22 @@ class KeyManager(object):
         @param ktype: The type of the key.
         @type ktype: KeyType
 
-        @raise httplib.HTTPException:
-        @raise KeyNotFound: If the key was not found both locally and in
-            keyserver.
+        @raise KeyNotFound: If the key was not found in local database.
         """
+        leap_assert(
+            ktype is OpenPGPKey,
+            'For now we only know how to send OpenPGP public keys.')
         # prepare the public key bound to address
         pubkey = self.get_key(
             self._address, ktype, private=False, fetch_remote=False)
         data = {
-            'address': self._address,
-            'keys': [
-                json.loads(pubkey.get_json()),
-            ]
+            self.PUBKEY_KEY: pubkey.key_data
         }
-        # prepare the private key bound to address
-        if send_private:
-            if password is None or password == '':
-                raise NoPasswordGiven('Can\'t send unencrypted private keys!')
-            privkey = self.get_key(
-                self._address, ktype, private=True, fetch_remote=False)
-            privkey = json.loads(privkey.get_json())
-            privkey.key_data = encrypt_sym(
-                privkey.key_data, passphrase=password)
-            data['keys'].append(privkey)
-        self._fetcher.put(
-            self._nickserver_url + '/key/' + self._address,
-            data=data,
-            auth=(self._address, self._token))
+        uri = "%s/%s/users/%s.json" % (
+            self._api_uri,
+            self._api_version,
+            self._uid)
+        self._put(uri, data)
 
     def get_key(self, address, ktype, private=False, fetch_remote=True):
         """
@@ -191,48 +272,15 @@ class KeyManager(object):
             ktype in self._wrapper_map,
             'Unkown key type: %s.' % str(ktype))
         try:
+            # return key if it exists in local database
             return self._wrapper_map[ktype].get_key(address, private=private)
         except KeyNotFound:
             # we will only try to fetch a key from nickserver if fetch_remote
             # is True and the key is not private.
             if fetch_remote is False or private is True:
                 raise
-            # fetch keys from server and discard unwanted types.
-            keys = filter(lambda k: isinstance(k, ktype),
-                          self.fetch_keys_from_server(address))
-            if len(keys) is 0:
-                raise KeyNotFound()
-            leap_assert(
-                len(keys) == 1,
-                'Got more than one key of type %s for %s.' %
-                (str(ktype), address))
-            self._wrapper_map[ktype].put_key(keys[0])
-            return self._wrapper_map[ktype].get_key(address, private=private)
-
-    def fetch_keys_from_server(self, address):
-        """
-        Fetch keys bound to C{address} from nickserver.
-
-        @param address: The address bound to the keys.
-        @type address: str
-
-        @return: A list of keys bound to C{address}.
-        @rtype: list of EncryptionKey
-        @raise KeyNotFound: If the key was not found on nickserver.
-        @raise httplib.HTTPException:
-        """
-        keydata = self._get_dict_from_http_json('/key/%s' % address)
-        leap_assert(
-            keydata['address'] == address,
-            "Fetched key for wrong address.")
-        keys = []
-        for key in keydata['keys']:
-            keys.append(
-                build_key_from_dict(
-                    self._key_class_from_type(key['type']),
-                    address,
-                    key))
-        return keys
+            self._fetch_keys_from_server(address)
+            return self._wrapper_map[ktype].get_key(address, private=False)
 
     def get_all_keys_in_local_db(self, private=False):
         """
@@ -248,7 +296,7 @@ class KeyManager(object):
                 doc.content),
             self._soledad.get_from_index(
                 TAGS_AND_PRIVATE_INDEX,
-                'keymanager-key',
+                KEYMANAGER_KEY_TAG,
                 '1' if private else '0'))
 
     def refresh_keys(self):
@@ -258,10 +306,11 @@ class KeyManager(object):
         addresses = set(map(
             lambda doc: doc.address,
             self.get_all_keys_in_local_db(private=False)))
-        # TODO: maybe we should not attempt to refresh our own public key?
         for address in addresses:
-            for key in self.fetch_keys_from_server(address):
-                self._wrapper_map[key.__class__].put_key(key)
+            # do not attempt to refresh our own key
+            if address == self._address:
+                continue
+            self._fetch_keys_from_server(address)
 
     def gen_key(self, ktype):
         """
@@ -276,14 +325,51 @@ class KeyManager(object):
         return self._wrapper_map[ktype].gen_key(self._address)
 
     #
-    # Token setter/getter
+    # Setters/getters
     #
 
-    def _get_token(self):
-        return self._token
+    def _get_session_id(self):
+        return self._session_id
+
+    def _set_session_id(self, session_id):
+        self._session_id = session_id
+
+    session_id = property(
+        _get_session_id, _set_session_id, doc='The session id.')
+
+    def _get_ca_cert_path(self):
+        return self._ca_cert_path
+
+    def _set_ca_cert_path(self, ca_cert_path):
+        self._ca_cert_path = ca_cert_path
+
+    ca_cert_path = property(
+        _get_ca_cert_path, _set_ca_cert_path,
+        doc='The path to the CA certificate.')
+
+    def _get_api_uri(self):
+        return self._api_uri
+
+    def _set_api_uri(self, api_uri):
+        self._api_uri = api_uri
+
+    api_uri = property(
+        _get_api_uri, _set_api_uri, doc='The webapp API URI.')
+
+    def _get_api_version(self):
+        return self._api_version
+
+    def _set_api_version(self, api_version):
+        self._api_version = api_version
+
+    api_version = property(
+        _get_api_version, _set_api_version, doc='The webapp API version.')
+
+    def _get_uid(self):
+        return self._uid
 
-    def _set_token(self, token):
-        self._token = token
+    def _set_uid(self, uid):
+        self._uid = uid
 
-    token = property(
-        _get_token, _set_token, doc='The auth token.')
+    uid = property(
+        _get_uid, _set_uid, doc='The uid of the user.')
index d98bd02..1d87858 100644 (file)
@@ -34,6 +34,30 @@ from leap.common.check import leap_assert
 
 
 #
+# Dictionary keys used for storing cryptographic keys.
+#
+
+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'
+KEY_LENGTH_KEY = 'length'
+KEY_EXPIRY_DATE_KEY = 'expiry_date'
+KEY_FIRST_SEEN_AT_KEY = 'first_seen_at'
+KEY_LAST_AUDITED_AT_KEY = 'last_audited_at'
+KEY_VALIDATION_KEY = 'validation'
+KEY_TAGS_KEY = 'tags'
+
+
+#
+# Key storage constants
+#
+
+KEYMANAGER_KEY_TAG = 'keymanager-key'
+
+#
 # Key handling utilities
 #
 
@@ -60,18 +84,20 @@ def build_key_from_dict(kClass, address, kdict):
     @return: An instance of the key.
     @rtype: C{kClass}
     """
-    leap_assert(address == kdict['address'], 'Wrong address in key data.')
+    leap_assert(
+        address == kdict[KEY_ADDRESS_KEY],
+        'Wrong address in key data.')
     return kClass(
         address,
-        key_id=kdict['key_id'],
-        fingerprint=kdict['fingerprint'],
-        key_data=kdict['key_data'],
-        private=kdict['private'],
-        length=kdict['length'],
-        expiry_date=kdict['expiry_date'],
-        first_seen_at=kdict['first_seen_at'],
-        last_audited_at=kdict['last_audited_at'],
-        validation=kdict['validation'],  # TODO: verify for validation.
+        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],
+        expiry_date=kdict[KEY_EXPIRY_DATE_KEY],
+        first_seen_at=kdict[KEY_FIRST_SEEN_AT_KEY],
+        last_audited_at=kdict[KEY_LAST_AUDITED_AT_KEY],
+        validation=kdict[KEY_VALIDATION_KEY],  # TODO: verify for validation.
     )
 
 
@@ -92,7 +118,7 @@ def keymanager_doc_id(ktype, address, private=False):
     """
     leap_assert(is_address(address), "Wrong address format: %s" % address)
     ktype = str(ktype)
-    visibility = 'private' if private else 'public'
+    visibility = KEY_PRIVATE_KEY if private else 'public'
     return sha256('keymanager-'+address+'-'+ktype+'-'+visibility).hexdigest()
 
 
@@ -141,18 +167,18 @@ class EncryptionKey(object):
         @rtype: str
         """
         return json.dumps({
-            'address': self.address,
-            '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,
-            'first_seen_at': self.first_seen_at,
-            'last_audited_at': self.last_audited_at,
-            'tags': ['keymanager-key'],
+            KEY_ADDRESS_KEY: self.address,
+            KEY_TYPE_KEY: str(self.__class__),
+            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: self.expiry_date,
+            KEY_VALIDATION_KEY: self.validation,
+            KEY_FIRST_SEEN_AT_KEY: self.first_seen_at,
+            KEY_LAST_AUDITED_AT_KEY: self.last_audited_at,
+            KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG],
         })
 
     def __repr__(self):
index a7aa1ca..dcd525c 100644 (file)
@@ -419,73 +419,106 @@ class KeyManagerKeyManagementTestCase(KeyManagerWithSoledadTestCase):
         km = self._key_manager()
         self.assertRaises(
             KeyNotFound,
-            km.send_key, OpenPGPKey, send_private=False)
+            km.send_key, OpenPGPKey)
 
-    def test_send_private_key_raises_key_not_found(self):
-        km = self._key_manager()
-        km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
-        self.assertRaises(
-            KeyNotFound,
-            km.send_key, OpenPGPKey, send_private=True,
-            password='123')
-
-    def test_send_private_key_without_password_raises(self):
-        km = self._key_manager()
-        km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
-        self.assertRaises(
-            NoPasswordGiven,
-            km.send_key, OpenPGPKey, send_private=True)
-
-    def test_send_public_key(self):
+    def test_send_key(self):
+        """
+        Test that request is well formed when sending keys to server.
+        """
         km = self._key_manager()
         km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
         km._fetcher.put = Mock()
-        km.token = '123'
-        km.send_key(OpenPGPKey, send_private=False)
-        # setup args
+        # the following data will be used on the send
+        km.ca_cert_path = 'capath'
+        km.session_id = 'sessionid'
+        km.uid = 'myuid'
+        km.api_uri = 'apiuri'
+        km.api_version = 'apiver'
+        km.send_key(OpenPGPKey)
+        # setup expected args
         data = {
-            'address': km._address,
-            'keys': [
-                json.loads(
-                    km.get_key(
-                        km._address, OpenPGPKey).get_json()),
-            ]
+            km.PUBKEY_KEY: km.get_key(km._address, OpenPGPKey).key_data,
         }
-        url = km._nickserver_url + '/key/' + km._address
-
+        url = '%s/%s/users/%s.json' % ('apiuri', 'apiver', 'myuid')
         km._fetcher.put.assert_called_once_with(
-            url, data=data, auth=(km._address, '123')
+            url, data=data, verify='capath',
+            cookies={'_session_id': 'sessionid'},
         )
 
-    def test_fetch_keys_from_server(self):
-        km = self._key_manager()
-        # setup mock
+    def test__fetch_keys_from_server(self):
+        """
+        Test that the request is well formed when fetching keys from server.
+        """
+        km = self._key_manager(url='http://nickserver.domain')
 
         class Response(object):
             status_code = 200
             headers = {'content-type': 'application/json'}
 
             def json(self):
-                return {'address': ADDRESS_2, 'keys': []}
+                return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2}
+
+            def raise_for_status(self):
+                pass
 
+        # mock the fetcher so it returns the key for ADDRESS_2
         km._fetcher.get = Mock(
             return_value=Response())
+        km.ca_cert_path = 'cacertpath'
         # do the fetch
-        km.fetch_keys_from_server(ADDRESS_2)
+        km._fetch_keys_from_server(ADDRESS_2)
         # and verify the call
         km._fetcher.get.assert_called_once_with(
-            km._nickserver_url + '/key/' + ADDRESS_2,
+            'http://nickserver.domain',
+            data={'address': ADDRESS_2},
+            verify='cacertpath',
         )
 
-    def test_refresh_keys(self):
-        # TODO: maybe we should not attempt to refresh our own public key?
+    def test_refresh_keys_does_not_refresh_own_key(self):
+        """
+        Test that refreshing keys will not attempt to refresh our own key.
+        """
         km = self._key_manager()
+        # we add 2 keys but we expect it to only refresh the second one.
         km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY)
-        km.fetch_keys_from_server = Mock(return_value=[])
+        km._wrapper_map[OpenPGPKey].put_ascii_key(PUBLIC_KEY_2)
+        # mock the key fetching
+        km._fetch_keys_from_server = Mock(return_value=[])
+        km.ca_cert_path = ''  # some bogus path so the km does not complain.
+        # do the refreshing
         km.refresh_keys()
-        km.fetch_keys_from_server.assert_called_once_with(
-            ADDRESS
+        km._fetch_keys_from_server.assert_called_once_with(
+            ADDRESS_2
+        )
+
+    def test_get_key_fetches_from_server(self):
+        """
+        Test that getting a key successfuly fetches from server.
+        """
+        km = self._key_manager(url='http://nickserver.domain')
+
+        class Response(object):
+            status_code = 200
+            headers = {'content-type': 'application/json'}
+
+            def json(self):
+                return {'address': ADDRESS_2, 'openpgp': PUBLIC_KEY_2}
+
+            def raise_for_status(self):
+                pass
+
+        # mock the fetcher so it returns the key for ADDRESS_2
+        km._fetcher.get = Mock(return_value=Response())
+        km.ca_cert_path = 'cacertpath'
+        # try to key get without fetching from server
+        self.assertRaises(
+            KeyNotFound, km.get_key, ADDRESS_2, OpenPGPKey,
+            fetch_remote=False
         )
+        # try to get key fetching from server.
+        key = km.get_key(ADDRESS_2, OpenPGPKey)
+        self.assertIsInstance(key, OpenPGPKey)
+        self.assertEqual(ADDRESS_2, key.address)
 
 
 # Key material for testing