diff options
Diffstat (limited to 'tests/keymanager/test_keymanager.py')
| -rw-r--r-- | tests/keymanager/test_keymanager.py | 609 | 
1 files changed, 609 insertions, 0 deletions
| diff --git a/tests/keymanager/test_keymanager.py b/tests/keymanager/test_keymanager.py new file mode 100644 index 00000000..b4ab805c --- /dev/null +++ b/tests/keymanager/test_keymanager.py @@ -0,0 +1,609 @@ +# -*- coding: utf-8 -*- +# test_keymanager.py +# Copyright (C) 2013 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 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 import defer +from twisted.trial import unittest +from twisted.web._responses import NOT_FOUND + +from leap.keymanager import client + +from leap.keymanager import errors +from leap.keymanager.keys import ( +    OpenPGPKey, +    is_address, +    build_key_from_dict, +) +from leap.keymanager.validation import ValidationLevels + +from common import ( +    KeyManagerWithSoledadTestCase, +    ADDRESS, +    ADDRESS_2, +    KEY_FINGERPRINT, +    PUBLIC_KEY, +    PUBLIC_KEY_2, +    PRIVATE_KEY, +    PRIVATE_KEY_2, +) + + +NICKSERVER_URI = "http://leap.se/" +REMOTE_KEY_URL = "http://site.domain/key" +INVALID_MAIL_ADDRESS = "notexistingemail@example.org" + + +class KeyManagerUtilTestCase(unittest.TestCase): + +    def test_is_address(self): +        self.assertTrue( +            is_address('user@leap.se'), +            'Incorrect address detection.') +        self.assertFalse( +            is_address('userleap.se'), +            'Incorrect address detection.') +        self.assertFalse( +            is_address('user@'), +            'Incorrect address detection.') +        self.assertFalse( +            is_address('@leap.se'), +            'Incorrect address detection.') + +    def test_build_key_from_dict(self): +        kdict = { +            'uids': [ADDRESS], +            'fingerprint': KEY_FINGERPRINT, +            'key_data': PUBLIC_KEY, +            'private': False, +            'length': 4096, +            'expiry_date': 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(kdict, adict) +        self.assertEqual( +            kdict['uids'], key.uids, +            'Wrong data in key.') +        self.assertEqual( +            kdict['fingerprint'], key.fingerprint, +            'Wrong data in key.') +        self.assertEqual( +            kdict['key_data'], key.key_data, +            'Wrong data in key.') +        self.assertEqual( +            kdict['private'], key.private, +            'Wrong data in key.') +        self.assertEqual( +            kdict['length'], key.length, +            'Wrong data in key.') +        self.assertEqual( +            None, key.expiry_date, +            'Wrong data in key.') +        self.assertEqual( +            None, key.last_audited_at, +            'Wrong data in key.') +        self.assertEqual( +            datetime.fromtimestamp(kdict['refreshed_at']), key.refreshed_at, +            'Wrong data in key.') +        self.assertEqual( +            adict['address'], key.address, +            'Wrong data in key.') +        self.assertEqual( +            ValidationLevels.get(adict['validation']), key.validation, +            'Wrong data in key.') +        self.assertEqual( +            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): + +    @defer.inlineCallbacks +    def _test_gen_key(self): +        km = self._key_manager() +        key = yield km.gen_key() +        self.assertIsInstance(key, OpenPGPKey) +        self.assertEqual( +            'leap@leap.se', key.address, 'Wrong address bound to key.') +        self.assertEqual( +            4096, key.length, 'Wrong key length.') + +    @defer.inlineCallbacks +    def test_get_all_keys_in_db(self): +        km = self._key_manager() +        yield km._openpgp.put_raw_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].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].uids) +        self.assertTrue(keys[0].private) + +    @defer.inlineCallbacks +    def test_get_public_key(self): +        km = self._key_manager() +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        # get the key +        key = yield km.get_key(ADDRESS, private=False, fetch_remote=False) +        self.assertTrue(key is not None) +        self.assertTrue(ADDRESS in key.uids) +        self.assertEqual( +            key.fingerprint.lower(), KEY_FINGERPRINT.lower()) +        self.assertFalse(key.private) + +    @defer.inlineCallbacks +    def test_get_public_key_with_binary_private_key(self): +        km = self._key_manager() +        yield km._openpgp.put_raw_key(self.get_private_binary_key(), ADDRESS) +        # get the key +        key = yield km.get_key(ADDRESS, private=False, fetch_remote=False) +        self.assertTrue(key is not None) +        self.assertTrue(ADDRESS in key.uids) +        self.assertEqual( +            key.fingerprint.lower(), KEY_FINGERPRINT.lower()) +        self.assertFalse(key.private) + +    @defer.inlineCallbacks +    def test_get_private_key(self): +        km = self._key_manager() +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        # get the key +        key = yield km.get_key(ADDRESS, private=True, fetch_remote=False) +        self.assertTrue(key is not None) +        self.assertTrue(ADDRESS in key.uids) +        self.assertEqual( +            key.fingerprint.lower(), KEY_FINGERPRINT.lower()) +        self.assertTrue(key.private) + +    def test_send_key_raises_key_not_found(self): +        km = self._key_manager() +        d = km.send_key() +        return self.assertFailure(d, errors.KeyNotFound) + +    @defer.inlineCallbacks +    def test_send_key(self): +        """ +        Test that request is well formed when sending keys to server. +        """ +        token = "mytoken" +        km = self._key_manager(token=token) +        yield km._openpgp.put_raw_key(PUBLIC_KEY, ADDRESS) +        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' +        km.uid = 'myuid' +        km.api_uri = 'apiuri' +        km.api_version = 'apiver' +        yield km.send_key() +        # setup expected args +        pubkey = yield km.get_key(km._address) +        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._async_client_pinned.request.assert_called_once_with( +            str(url), 'PUT', body=str(data), +            headers=headers +        ) + +    def test_fetch_keys_from_server(self): +        """ +        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(_): +            used_kwargs = km._async_client_pinned.request.call_args[1] +            km._async_client_pinned.request.assert_called_once_with( +                expected_url, 'GET', **used_kwargs) + +        d = self._fetch_key(km, ADDRESS_2, PUBLIC_KEY_2) +        d.addCallback(verify_the_call) +        return d + +    def test_key_not_found_is_raised_if_key_search_responds_404(self): +        """ +        Test if key search request comes back with a 404 response then +        KeyNotFound is raised, with corresponding error message. +        """ +        km = self._key_manager(url=NICKSERVER_URI) +        client.readBody = Mock(return_value=defer.succeed(None)) +        km._async_client_pinned.request = Mock( +            return_value=defer.succeed(None)) +        url = NICKSERVER_URI + '?address=' + INVALID_MAIL_ADDRESS + +        d = km._fetch_and_handle_404_from_nicknym(url, INVALID_MAIL_ADDRESS) + +        def check_key_not_found_is_raised_if_404(_): +            used_kwargs = km._async_client_pinned.request.call_args[1] +            check_404_callback = used_kwargs['callback'] +            fake_response = Mock() +            fake_response.code = NOT_FOUND +            with self.assertRaisesRegexp( +                    errors.KeyNotFound, +                    '404: %s key not found.' % INVALID_MAIL_ADDRESS): +                check_404_callback(fake_response) + +        d.addCallback(check_key_not_found_is_raised_if_404) +        return d + +    def test_non_existing_key_from_nicknym_is_relayed(self): +        """ +        Test if key search requests throws KeyNotFound, the same error is +        raised. +        """ +        km = self._key_manager(url=NICKSERVER_URI) +        key_not_found_exception = errors.KeyNotFound('some message') +        km._async_client_pinned.request = Mock( +            side_effect=key_not_found_exception) + +        def assert_key_not_found_raised(error): +            self.assertEqual(error.value, key_not_found_exception) + +        d = km._get_key_from_nicknym(INVALID_MAIL_ADDRESS) +        d.addErrback(assert_key_not_found_raised) + +    @defer.inlineCallbacks +    def test_get_key_fetches_from_server(self): +        """ +        Test that getting a key successfuly fetches from server. +        """ +        km = self._key_manager(url=NICKSERVER_URI) + +        key = yield self._fetch_key(km, ADDRESS, PUBLIC_KEY) +        self.assertIsInstance(key, OpenPGPKey) +        self.assertTrue(ADDRESS in key.uids) +        self.assertEqual(key.validation, ValidationLevels.Provider_Trust) + +    @defer.inlineCallbacks +    def test_get_key_fetches_other_domain(self): +        """ +        Test that getting a key successfuly fetches from server. +        """ +        km = self._key_manager(url=NICKSERVER_URI) + +        key = yield self._fetch_key(km, ADDRESS_OTHER, PUBLIC_KEY_OTHER) +        self.assertIsInstance(key, OpenPGPKey) +        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 +        """ +        data = json.dumps({'address': address, 'openpgp': key}) + +        client.readBody = Mock(return_value=defer.succeed(data)) + +        # mock the fetcher so it returns the key for ADDRESS_2 +        km._async_client_pinned.request = Mock( +            return_value=defer.succeed(None)) +        km.ca_cert_path = 'cacertpath' +        # try to key get without fetching from server +        d_fail = km.get_key(address, fetch_remote=False) +        d = self.assertFailure(d_fail, errors.KeyNotFound) +        # try to get key fetching from server. +        d.addCallback(lambda _: km.get_key(address)) +        return d + +    @defer.inlineCallbacks +    def test_put_key_ascii(self): +        """ +        Test that putting ascii key works +        """ +        km = self._key_manager(url=NICKSERVER_URI) + +        yield km.put_raw_key(PUBLIC_KEY, ADDRESS) +        key = yield km.get_key(ADDRESS) +        self.assertIsInstance(key, OpenPGPKey) +        self.assertTrue(ADDRESS in key.uids) + +    @defer.inlineCallbacks +    def test_put_key_binary(self): +        """ +        Test that putting binary key works +        """ +        km = self._key_manager(url=NICKSERVER_URI) + +        yield km.put_raw_key(self.get_public_binary_key(), ADDRESS) +        key = yield km.get_key(ADDRESS) + +        self.assertIsInstance(key, OpenPGPKey) +        self.assertTrue(ADDRESS in key.uids) + +    @defer.inlineCallbacks +    def test_fetch_uri_ascii_key(self): +        """ +        Test that fetch key downloads the ascii key and gets included in +        the local storage +        """ +        km = self._key_manager() + +        km._async_client.request = Mock(return_value=defer.succeed(PUBLIC_KEY)) + +        yield km.fetch_key(ADDRESS, "http://site.domain/key") +        key = yield km.get_key(ADDRESS) +        self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + +    @defer.inlineCallbacks +    def test_fetch_uri_binary_key(self): +        """ +        Test that fetch key downloads the binary key and gets included in +        the local storage +        """ +        km = self._key_manager() + +        km._async_client.request = Mock( +            return_value=defer.succeed(self.get_public_binary_key())) + +        yield km.fetch_key(ADDRESS, "http://site.domain/key") +        key = yield km.get_key(ADDRESS) +        self.assertEqual(KEY_FINGERPRINT, key.fingerprint) + +    def test_fetch_uri_empty_key(self): +        """ +        Test that fetch key raises KeyNotFound if no key in the url +        """ +        km = self._key_manager() + +        km._async_client.request = Mock(return_value=defer.succeed("")) +        d = km.fetch_key(ADDRESS, "http://site.domain/key") +        return self.assertFailure(d, errors.KeyNotFound) + +    def test_fetch_uri_address_differ(self): +        """ +        Test that fetch key raises KeyAttributesDiffer if the address +        don't match +        """ +        km = self._key_manager() + +        km._async_client.request = Mock(return_value=defer.succeed(PUBLIC_KEY)) +        d = km.fetch_key(ADDRESS_2, "http://site.domain/key") +        return self.assertFailure(d, errors.KeyAddressMismatch) + +    def _mock_get_response(self, km, body): +        km._async_client.request = MagicMock(return_value=defer.succeed(body)) + +        return km._async_client.request + +    @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) +        get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER) + +        yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL) + +        get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') + +    @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) +        get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER) + +        yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL) + +        get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') + +    @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) +        get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER) + +        yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL) + +        get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') + +    @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 = pkg_resources.resource_string('leap.common.testing', +                                                       'cacert.pem') +            ca_cert_path = tmp_input.name +            self._dump_to_file(ca_cert_path, ca_content) + +            with patch('leap.keymanager.tempfile.NamedTemporaryFile') as mock: +                mock.return_value = tmp_output +                km = self._key_manager(ca_cert_path=ca_cert_path) +                get_mock = self._mock_get_response(km, PUBLIC_KEY_OTHER) + +                yield km.fetch_key(ADDRESS_OTHER, REMOTE_KEY_URL) + +                # assert that combined bundle file is passed to get call +                get_mock.assert_called_once_with(REMOTE_KEY_URL, 'GET') + +                # assert that files got appended +                expected = self._slurp_file(ca_bundle.where()) + ca_content +                self.assertEqual(expected, self._slurp_file(tmp_output.name)) + +            del km  # force km out of scope +            self.assertFalse(path.exists(tmp_output.name)) + +    def _dump_to_file(self, filename, content): +            with open(filename, 'w') as out: +                out.write(content) + +    def _slurp_file(self, filename): +        with open(filename) as f: +            content = f.read() +        return content + +    @defer.inlineCallbacks +    def test_decrypt_updates_sign_used_for_signer(self): +        # given +        km = self._key_manager() +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        yield km._openpgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) +        encdata = yield km.encrypt('data', ADDRESS, sign=ADDRESS_2, +                                   fetch_remote=False) +        yield km.decrypt( +            encdata, ADDRESS, verify=ADDRESS_2, fetch_remote=False) + +        # when +        key = yield km.get_key(ADDRESS_2, fetch_remote=False) + +        # then +        self.assertEqual(True, key.sign_used) + +    @defer.inlineCallbacks +    def test_decrypt_does_not_update_sign_used_for_recipient(self): +        # given +        km = self._key_manager() +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        yield km._openpgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) +        encdata = yield km.encrypt('data', ADDRESS, sign=ADDRESS_2, +                                   fetch_remote=False) +        yield km.decrypt( +            encdata, ADDRESS, verify=ADDRESS_2, fetch_remote=False) + +        # when +        key = yield km.get_key( +            ADDRESS, private=False, fetch_remote=False) + +        # then +        self.assertEqual(False, key.sign_used) + + +class KeyManagerCryptoTestCase(KeyManagerWithSoledadTestCase): + +    RAW_DATA = 'data' + +    @defer.inlineCallbacks +    def test_keymanager_openpgp_encrypt_decrypt(self): +        km = self._key_manager() +        # put raw private key +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        yield km._openpgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) +        # encrypt +        encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, sign=ADDRESS_2, +                                   fetch_remote=False) +        self.assertNotEqual(self.RAW_DATA, encdata) +        # decrypt +        rawdata, signingkey = yield km.decrypt( +            encdata, ADDRESS, verify=ADDRESS_2, fetch_remote=False) +        self.assertEqual(self.RAW_DATA, rawdata) +        key = yield km.get_key(ADDRESS_2, private=False, fetch_remote=False) +        self.assertEqual(signingkey.fingerprint, key.fingerprint) + +    @defer.inlineCallbacks +    def test_keymanager_openpgp_encrypt_decrypt_wrong_sign(self): +        km = self._key_manager() +        # put raw keys +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        yield km._openpgp.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) +        # encrypt +        encdata = yield km.encrypt(self.RAW_DATA, ADDRESS, sign=ADDRESS_2, +                                   fetch_remote=False) +        self.assertNotEqual(self.RAW_DATA, encdata) +        # verify +        rawdata, signingkey = yield km.decrypt( +            encdata, ADDRESS, verify=ADDRESS, fetch_remote=False) +        self.assertEqual(self.RAW_DATA, rawdata) +        self.assertTrue(isinstance(signingkey, errors.InvalidSignature)) + +    @defer.inlineCallbacks +    def test_keymanager_openpgp_sign_verify(self): +        km = self._key_manager() +        # put raw private keys +        yield km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        signdata = yield km.sign(self.RAW_DATA, ADDRESS, detach=False) +        self.assertNotEqual(self.RAW_DATA, signdata) +        # verify +        signingkey = yield km.verify(signdata, ADDRESS, fetch_remote=False) +        key = yield km.get_key(ADDRESS, private=False, fetch_remote=False) +        self.assertEqual(signingkey.fingerprint, key.fingerprint) + +    def test_keymanager_encrypt_key_not_found(self): +        km = self._key_manager() +        d = km._openpgp.put_raw_key(PRIVATE_KEY, ADDRESS) +        d.addCallback( +            lambda _: km.encrypt(self.RAW_DATA, ADDRESS_2, sign=ADDRESS, +                                 fetch_remote=False)) +        return self.assertFailure(d, errors.KeyNotFound) + +if __name__ == "__main__": +    import unittest +    unittest.main() + +# key 0F91B402: someone@somedomain.org +# 9420 EC7B 6DCB 867F 5592  E6D1 7504 C974 0F91 B402 +ADDRESS_OTHER = "someone@somedomain.org" +PUBLIC_KEY_OTHER = """ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mQENBFUZFLwBCADRzTstykRAV3aWysLAV4O3DXdpXhV3Cww8Pfc6m1bVxAT2ifcL +kLWEaIkOB48SYIHbYzqOi1/h5abJf+5n4uhaIks+FsjsXYo1XOiYpVCNf7+xLnUM +jkmglKT5sASr61QDcFMqWfGTJ8iUTNVCJZ2k14QJ4Vss/ntnV9uB7Ef7wU7RZvxr +wINH/0LfKPsGE9l2qNpKUAAmg2bHn9YdsHj1sqlW7eZpwvefYrQej4KBaL2oq3vt +QQOdXGFqWYMe3cX+bQ1DAMG3ttTF6EGkY97BK7A18I/RJiLujWCEAkMzFr5SK9KU +AOMj6MpjfTOE+GfUKsu7/gGt42eMBFsIOvsZABEBAAG0IFNvbWVvbmUgPHNvbWVv +bmVAc29tZWRvbWFpbi5vcmc+iQE4BBMBAgAiBQJVGRS8AhsDBgsJCAcDAgYVCAIJ +CgsEFgIDAQIeAQIXgAAKCRB1BMl0D5G0AlFsCAC33LhxBRwO64T6DgTb4/39aLpi +9T3yAmXBAHC7Q+4f37IBX5fJBRKu4Lvfp6KherOl/I/Jj34yv8pm0j+kXeWktfxZ +cW+mv2vjBHQVopiUSyMVh7caFSq9sKm+oQdo6oIl9DHSARegbkCn2+0b4VxgJpyj +TZBMyUMD2AayivQU4QHOM3KCozhLNNDbpKy7LH0MSAUDmRaJsPk1zK15lQocK/7R +Z5yF4rdrdzDWrVucZJc09yntSqTGECue3W2GBCaBlb/O1c9xei4MTb4nSHS5Gp/7 +hcjrvIrgPpehndk8ZRREN/Y8uk1W5fbWzx+5z8g31RCGWBQw4NAnG10NZ3oEuQEN +BFUZFLwBCADocYZmLu1iXIE6gKqniR6Z8UDC5XnqgK+BEJwi1abe9zWhjgKeW9Vv +u1i194wuCUiNkP/bMvwMBZLTslDzqxl32ETk9FvB3kWy80S8MDjQJ15IN4I622fq +MEWwtQ0WrRay9VV6M8H2mIf71/1d5T9ysWK4XRyv+N7eRhfg7T2uhrpNyKdCZzjq +2wlgpVkMY7gtxTqJseM+qS5UNiReGxtoOXFLzzmagFgbqK88eMeZJZt8yKf81xhP +SWLTxaVaeBEAlajvEkxZJrrDQuc+maTwtMxmNUe815wJnpcRF8VD91GUpSLAN6EC +1QuJUl6Lc2o2tcHeo6CGsDZ96o0J8pFhABEBAAGJAR8EGAECAAkFAlUZFLwCGwwA +CgkQdQTJdA+RtAKcdwgApzHPhwwaZ9TBjgOytke/hPE0ht/EJ5nRiIda2PucoPh6 +DwnaI8nvmGXUfC4qFy6LM8/fJHof1BqLnMbx8MCLurnm5z30q8RhLE3YWM11zuMy +6wkHGmi/6S1G4okC+Uu8AA4K//HBo8bLcqGVWRnFAmCqy6VMAofsQvmM7vHbRj56 +U919Bki/7I6kcxPEzO73Umh3o82VP/Hz3JMigRNBRfG3jPrX04RLJj3Ib5lhQIDw +XrO8VHz9foOpY+rJnWj+6QAozxorzZYShu6H0GR1nIuqWMwli1nrx6BeIJAVz5cg +QzEd9yAN+81fkIBaa6Y8LCBxV03JCc2J4eCUKXd1gg== +=gDzy +-----END PGP PUBLIC KEY BLOCK----- +""" | 
