mirror of
https://github.com/clearlinux/autospec.git
synced 2026-06-16 02:45:56 +00:00
90bcff8bf9
pypi is deprecating the API and doesn't recommend its usage for integrity checks so stop trying to make use of it in autospec. Signed-off-by: William Douglas <william.douglas@intel.com>
451 lines
20 KiB
Python
451 lines
20 KiB
Python
from io import BytesIO
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import config
|
|
import download
|
|
import pkg_integrity
|
|
|
|
|
|
TESTDIR = os.path.join(os.getcwd(), "tests/testfiles/pkg_integrity")
|
|
TESTKEYDIR = os.path.join(TESTDIR, "testkeys")
|
|
|
|
PACKAGE_URL = "http://pkgconfig.freedesktop.org/releases/pkg-config-0.29.1.tar.gz"
|
|
NO_SIGN_PKT_URL = "http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.25.tar.gz"
|
|
NOSIGN_PKT_URL_BAD = "http://gnu.mirrors.pair.com/savannah/savannah/quagga/bad_quagga-1.1.0.tar.gz"
|
|
NOSIGN_PKT_URL = "http://download.savannah.gnu.org/releases/quagga/quagga-1.1.0.tar.gz"
|
|
NOSIGN_SIGN_URL = "http://download.savannah.gnu.org/releases/quagga/quagga-1.1.0.tar.gz.asc"
|
|
GNOME_SHA256_PKG = "https://download.gnome.org/sources/pygobject/3.24/pygobject-3.24.0.tar.xz"
|
|
QT_SHA256_PKG = "https://download.qt.io/official_releases/qt/5.12/5.12.4/submodules/qtspeech-everywhere-src-5.12.4.tar.xz"
|
|
KEYID = "EC2392F2EDE74488680DA3CF5F2B4756ED873D23"
|
|
|
|
|
|
def mock_download_do_curl(url, dst=None):
|
|
bad_sigs = ["http://pkgconfig.freedesktop.org/releases/pkg-config-0.29.1.tar.gz.sig",
|
|
"http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.25.tar.gz.sig",
|
|
"http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.25.tar.gz.asc",
|
|
"http://www.ferzkopp.net/Software/SDL_gfx-2.0/SDL_gfx-2.0.25.tar.gz.sign"]
|
|
if not dst:
|
|
return BytesIO(b'foobar')
|
|
src = os.path.join(TESTDIR, os.path.basename(url))
|
|
if dst and os.path.isfile(src):
|
|
shutil.copyfile(src, dst)
|
|
return dst
|
|
else:
|
|
return None
|
|
if url in bad_sigs:
|
|
return None
|
|
|
|
|
|
@patch('download.do_curl', mock_download_do_curl)
|
|
class TestCheckFn(unittest.TestCase):
|
|
|
|
def test_check_matching_sign_url(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
pkey = "023A4420C7EC6914.pkey"
|
|
shutil.copy(os.path.join(TESTKEYDIR, pkey), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)) + ".asc", tmpd)
|
|
result = pkg_integrity.check(PACKAGE_URL, conf)
|
|
self.assertTrue(result)
|
|
|
|
def test_check_with_existing_sign(self):
|
|
""" Download signature for local verification """
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "6FE57CA8C1A4AEA6.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(NOSIGN_PKT_URL)), tmpd)
|
|
result = pkg_integrity.check(NOSIGN_PKT_URL, conf)
|
|
self.assertTrue(result)
|
|
|
|
|
|
@patch('download.do_curl', mock_download_do_curl)
|
|
class TestDomainBasedVerifiers(unittest.TestCase):
|
|
|
|
def run_test_for_domain(self, Verifier, url):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
filen = os.path.basename(url)
|
|
shutil.copy(os.path.join(TESTDIR, filen), tmpd)
|
|
package_path = os.path.join(tmpd, filen)
|
|
verifier = Verifier(**{'package_path': package_path,
|
|
'url': url})
|
|
return verifier.verify()
|
|
return None
|
|
|
|
def _mock_fetch_shasum(url):
|
|
return (
|
|
"100395496483fcea7ba03fc1655c7a770f7f2e12e93be8bda2e31fec42debde0 pygobject-3.24.0.news\n"
|
|
"ae417db3be2a197b403bba6472cfb35a6e642cd802660832acb9c96123f79463 pygobject-3.24.0.changes\n"
|
|
"4e228b1c0f36e810acd971fad1c7030014900d8427c308d63a560f3f1037fa3c pygobject-3.24.0.tar.xz"
|
|
)
|
|
|
|
@patch('pkg_integrity.GnomeOrgVerifier.fetch_shasum', _mock_fetch_shasum)
|
|
def test_gnome_org(self):
|
|
result = self.run_test_for_domain(pkg_integrity.GnomeOrgVerifier, GNOME_SHA256_PKG)
|
|
self.assertTrue(result)
|
|
|
|
@patch('pkg_integrity.QtIoVerifier.fetch_shasum')
|
|
def test_qt_io(self, test_fetch):
|
|
test_fetch.return_value = "2ff9660fb3f5663c9161f491d1a304db62691720136ae22c145ef6a1c94b90ec qtspeech-everywhere-src-5.12.4.tar.xz\n"
|
|
result = self.run_test_for_domain(pkg_integrity.QtIoVerifier, QT_SHA256_PKG)
|
|
self.assertTrue(result)
|
|
|
|
|
|
@patch('download.do_curl', mock_download_do_curl)
|
|
class TestGPGVerifier(unittest.TestCase):
|
|
|
|
def test_from_url(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "023A4420C7EC6914.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)), tmpd)
|
|
result = pkg_integrity.check(PACKAGE_URL, conf)
|
|
self.assertTrue(result)
|
|
|
|
def test_invalid_key(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "6FE57CA8C1A4AEA6.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(NOSIGN_PKT_URL_BAD)), tmpd)
|
|
with open(os.path.join(tmpd, os.path.basename(NOSIGN_PKT_URL_BAD) + ".asc"), 'w') as ofile:
|
|
ofile.write("Invalid signature")
|
|
result = pkg_integrity.check(NOSIGN_PKT_URL_BAD, conf)
|
|
self.assertIsNone(result)
|
|
|
|
def test_key_not_found(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "6FE57CA8C1A4AEA6.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(NOSIGN_PKT_URL_BAD)), tmpd)
|
|
result = pkg_integrity.check(NOSIGN_PKT_URL_BAD, conf)
|
|
self.assertIsNone(result)
|
|
|
|
def test_from_disk(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "023A4420C7EC6914.pkey"), tmpd)
|
|
out_file = os.path.join(tmpd, os.path.basename(PACKAGE_URL))
|
|
out_key = out_file + ".asc"
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)) + ".asc", tmpd)
|
|
result = pkg_integrity.from_disk(PACKAGE_URL, out_file, out_key, conf)
|
|
self.assertTrue(result)
|
|
|
|
def test_non_matchingsig(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "023A4420C7EC6914.pkey"), tmpd)
|
|
out_file = os.path.join(tmpd, os.path.basename(PACKAGE_URL))
|
|
f = open(out_file, 'wb')
|
|
f.write(b'made up date that will fail check')
|
|
f.close()
|
|
with self.assertRaises(SystemExit) as a:
|
|
pkg_integrity.check(PACKAGE_URL, conf)
|
|
self.assertEqual(a.exception.code, 1)
|
|
|
|
def test_result_on_non_existent_pkg_path(self):
|
|
conf = config.Config('')
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
result = pkg_integrity.from_disk('http://nokey.com/package.tar.gz',
|
|
'NonExistentPKG.tar.gz',
|
|
'NonExistentKey.asc',
|
|
conf)
|
|
self.assertIsNone(result)
|
|
|
|
def test_result_on_nosign_package(self):
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(NOSIGN_PKT_URL)), tmpd)
|
|
result = pkg_integrity.check(NO_SIGN_PKT_URL, conf)
|
|
self.assertIsNone(result)
|
|
|
|
@patch.object(pkg_integrity.GPGCli, 'exec_cmd')
|
|
@patch('pkg_integrity.parse_gpg_packets')
|
|
def test_result_multiple_sig(self, mock_parse, mock_exec):
|
|
"""Test verification of first signature of tarball with multiple signatures."""
|
|
def packets_separator(filename, **kwargs):
|
|
if filename.endswith('.pkey'):
|
|
packets = [
|
|
{
|
|
'offset': 528,
|
|
'length': 37,
|
|
'type': 'user ID',
|
|
'email': 'user1@example.com',
|
|
},
|
|
]
|
|
elif filename.endswith('.asc'):
|
|
packets = [
|
|
{
|
|
'offset': 0,
|
|
'length': 543,
|
|
'type': 'signature',
|
|
'keyid': '023A4420C7EC6914',
|
|
},
|
|
{
|
|
'offset': 543,
|
|
'length': 543,
|
|
'type': 'signature',
|
|
'keyid': '12345678DEADCAFE',
|
|
},
|
|
]
|
|
return packets
|
|
mock_parse.side_effect = packets_separator
|
|
|
|
mock_exec.return_value = (b'', b'', 0)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "023A4420C7EC6914.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)), tmpd)
|
|
result = pkg_integrity.check(PACKAGE_URL, conf)
|
|
self.assertTrue(result)
|
|
self.assertEqual(mock_parse.call_count, 4)
|
|
self.assertEqual(mock_exec.call_count, 3)
|
|
self.assertEqual(pkg_integrity.EMAIL, "user1@example.com")
|
|
self.assertEqual(pkg_integrity.KEYID, "023A4420C7EC6914")
|
|
|
|
@patch.object(pkg_integrity.GPGCli, 'exec_cmd')
|
|
@patch('pkg_integrity.parse_gpg_packets')
|
|
def test_result_multiple_sig_no_separators(self, mock_parse, mock_exec):
|
|
"""Test skipping of sig verification in the multiple sig case when packet separators are absent."""
|
|
def packets_no_separator(filename, **kwargs):
|
|
if filename.endswith('.pkey'):
|
|
packets = [
|
|
{
|
|
'type': 'user ID',
|
|
'email': 'user2@example.com',
|
|
},
|
|
]
|
|
elif filename.endswith('.asc'):
|
|
packets = [
|
|
{
|
|
'type': 'signature',
|
|
'keyid': '023A4420C7EC6914',
|
|
},
|
|
{
|
|
'type': 'signature',
|
|
'keyid': 'DEADCAFEC7EC6914',
|
|
},
|
|
]
|
|
return packets
|
|
mock_parse.side_effect = packets_no_separator
|
|
|
|
mock_exec.return_value = (b'', b'', 0)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpd:
|
|
conf = config.Config(tmpd)
|
|
conf.rewrite_config_opts = unittest.mock.Mock()
|
|
conf.config_opts['verify_required'] = False
|
|
shutil.copy(os.path.join(TESTKEYDIR, "023A4420C7EC6914.pkey"), tmpd)
|
|
shutil.copy(os.path.join(TESTDIR, os.path.basename(PACKAGE_URL)), tmpd)
|
|
with self.assertRaises(SystemExit) as msg:
|
|
result = pkg_integrity.check(PACKAGE_URL, conf)
|
|
self.assertEqual(msg.exception.code, 1)
|
|
self.assertEqual(mock_parse.call_count, 4)
|
|
self.assertEqual(mock_exec.call_count, 2)
|
|
self.assertEqual(pkg_integrity.EMAIL, "user2@example.com")
|
|
self.assertEqual(pkg_integrity.KEYID, "023A4420C7EC6914")
|
|
|
|
|
|
class TestInputGetter(unittest.TestCase):
|
|
|
|
def test_timput(self):
|
|
ig = pkg_integrity.InputGetter(default='N', timeout=2)
|
|
answer = ig.get_answer()
|
|
self.assertIsNone(answer)
|
|
ig = pkg_integrity.InputGetter(default='Y', timeout=2)
|
|
answer = ig.get_answer()
|
|
self.assertIsNone(answer)
|
|
|
|
|
|
class TestUtils(unittest.TestCase):
|
|
|
|
def test_get_verifier(self):
|
|
x = pkg_integrity.get_verifier('file.abcd')
|
|
self.assertEqual(x, None)
|
|
|
|
y = pkg_integrity.get_verifier('xorriso-1.4.6.tar.gz')(
|
|
package_path='',
|
|
url='http://ftp.gnu.org/gnu/xorriso/xorriso-1.4.6.tar.gz',
|
|
package_check='http://ftp.gnu.org/gnu/xorriso/xorriso-1.4.6.tar.gz.asc'
|
|
)
|
|
self.assertTrue(isinstance(y, pkg_integrity.GPGVerifier))
|
|
|
|
def test_parse_gpg_packets_for_keyid(self):
|
|
"""Test parse_gpg_packets() to retrieve keyid info."""
|
|
def check_packets(algo, key_id, packet_count, packet_with_val):
|
|
with tempfile.NamedTemporaryFile(delete=True) as tmpf:
|
|
tmpf.write(algo)
|
|
tmpf.flush()
|
|
packets = pkg_integrity.parse_gpg_packets(tmpf.name)
|
|
self.assertIsNotNone(packets)
|
|
self.assertEqual(len(packets), packet_count)
|
|
self.assertEqual(packets[packet_with_val]["keyid"], key_id)
|
|
tmpf.close()
|
|
|
|
check_packets(KEY_ALGO17, '8AFAFCD242818A52', 6, 1)
|
|
check_packets(KEY_ALGO1, '330239C1C4DAFEE1', 1, 0)
|
|
|
|
def test_get_keyid(self):
|
|
"""Test get_keyid() to retrieve key ID from GPG key or signature."""
|
|
def check_get_keyid(algo, key_id):
|
|
with tempfile.NamedTemporaryFile(delete=True) as tmpf:
|
|
tmpf.write(algo)
|
|
tmpf.flush()
|
|
result = pkg_integrity.get_keyid(tmpf.name)
|
|
self.assertEqual(result, key_id)
|
|
tmpf.close()
|
|
|
|
check_get_keyid(KEY_ALGO17, '8AFAFCD242818A52')
|
|
check_get_keyid(KEY_ALGO1, '330239C1C4DAFEE1')
|
|
|
|
def test_get_keyid_none(self):
|
|
"""Test get_keyid() when the key name is invalid."""
|
|
false_name = '/false/name'
|
|
self.assertTrue(pkg_integrity.get_keyid(false_name) is None)
|
|
|
|
def _mock_download_file(url, dst=None):
|
|
# make return codes match by url to ensure we are using the expected signature type
|
|
if url in ("http://ftp.gnu.org/pub/gnu/gperf/gperf-3.0.4.tar.gz.sig",
|
|
"http://download.savannah.gnu.org/releases/quilt/quilt-0.65.tar.gz.asc",
|
|
"http://download.savannah.gnu.org/releases/freetype/freetype-2.9.tar.bz2.sign"):
|
|
return os.path.join(dst, os.path.basename(url))
|
|
return None
|
|
|
|
@patch('download.do_curl', _mock_download_file)
|
|
def test_get_signature_url(self):
|
|
url_from_gnu = "http://ftp.gnu.org/pub/gnu/gperf/gperf-3.0.4.tar.gz"
|
|
url_from_gnu1 = "http://download.savannah.gnu.org/releases/quilt/quilt-0.65.tar.gz"
|
|
url_from_gnu2 = "http://download.savannah.gnu.org/releases/freetype/freetype-2.9.tar.bz2"
|
|
|
|
self.assertEqual(pkg_integrity.get_signature_file(url_from_gnu, '.')[-4:], '.sig')
|
|
self.assertEqual(pkg_integrity.get_signature_file(url_from_gnu1, '.')[-4:], '.asc')
|
|
self.assertEqual(pkg_integrity.get_signature_file(url_from_gnu2, '.')[-5:], '.sign')
|
|
|
|
def test_parse_gpg_packets_for_email(self):
|
|
"""Test parse_gpg_packets() to retrieve email info."""
|
|
def check_packets(algo, email, packet_count, packet_with_val):
|
|
with tempfile.NamedTemporaryFile(delete=True) as tmpf:
|
|
tmpf.write(algo)
|
|
tmpf.flush()
|
|
packets = pkg_integrity.parse_gpg_packets(tmpf.name)
|
|
self.assertIsNotNone(packets)
|
|
self.assertEqual(len(packets), packet_count)
|
|
if packet_with_val:
|
|
self.assertEqual(packets[packet_with_val]["email"], email)
|
|
tmpf.close()
|
|
|
|
check_packets(KEY_ALGO17, 'kislyuk@gmail.com', 6, 0)
|
|
check_packets(KEY_ALGO1, None, 1, None)
|
|
|
|
def test_get_email(self):
|
|
"""Test get_email() to retrieve email info from GPG key."""
|
|
def check_get_email(algo, email):
|
|
with tempfile.NamedTemporaryFile(delete=True) as tmpf:
|
|
tmpf.write(algo)
|
|
tmpf.flush()
|
|
result = pkg_integrity.get_email(tmpf.name)
|
|
self.assertEqual(result, email)
|
|
tmpf.close()
|
|
|
|
check_get_email(KEY_ALGO17, 'kislyuk@gmail.com')
|
|
check_get_email(KEY_ALGO1, None)
|
|
|
|
KEY_ALGO1 = b"""\
|
|
-----BEGIN PGP SIGNATURE-----
|
|
|
|
iQEcBAABAgAGBQJX1yrCAAoJEDMCOcHE2v7hwhcH/AqhJ/vqkCOo09Yh89bYRFyb
|
|
O5yEpaRV7vo4qoqXht3fQiR3KC3lSKybtScihbQ0xcTeBzSwGPMKPRpOqXEKRnwY
|
|
9Zq8ev72Ixi5yVsKdSKjoeM4smXJdQolKnrKy0chsOMzu7cxk7hwejplIMjycKza
|
|
g3HM6jtw6v10JDj6a5SJPkrufj2eIHq1enn2di9q9+yyAJGiuWi3ABfINmL3Y9sa
|
|
zODoMBD/B7LCZ1Zv6dG7kaN1XS82crOmXrtgcWslKxfGPkdW9SGQdhCtm6f/Z5/w
|
|
29adzXFicobZBFBHVnbQ0iRf39omkNxPOhMYwLVQFprzjOPm/DDHSztZj87jbOM=
|
|
=Qe1y
|
|
-----END PGP SIGNATURE-----
|
|
"""
|
|
|
|
KEY_ALGO17 = b"""\
|
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
Version: GnuPG v1
|
|
|
|
mQGiBEYdIyURBACrG5G9L7R2uLGnCer+liVhsn5vwJIwIpLtVTb7Z3AcOoumza66
|
|
5JBZtY9RSsNRbMcuqxPWeXnl8jhs513O0dZihTL+/cLMD1RJAXRlf0eDxAYl2TiD
|
|
7AdH+y6HGHljn2IkH5jIzdTgNXdJZ8BArkFZP7an+ZLfE5RgSQ4QqZpuxwCgiq9+
|
|
fjVoHSvGJHlcN4SiBBn8cCkEAKsYT7rp7N2GaA6b4XjLr2bVnv3MV431EhxTHynX
|
|
mcnpT91ItMAkjPWcMSWWBhYJ/rgi2KYh5oAMhcLt2gCU58TxMsxA2rPEocHFDzKY
|
|
Yj9HOXCa4XasL4R+Jyu4/Flh7Fqvrts5iZViDx/hPo1O5HhM7VuVnDxOc6yVPDB1
|
|
OGj5A/9lCCn2awr4XxEWsYnBDtoqzfb8jwIDfS4xtqCAaegiXr4P1GGgkDtlW0XC
|
|
5k2giFiqD0l928y7YV7Mumw2hjqECyPwDDbsEMfJasWEnAG7hBmAfHtIwoC8563d
|
|
3QM16Wu33xXB+rbss/mH95oUOEtkR1m9HCAOZ+R/T9WVdwTKN7QiQW5kcmV5IEtp
|
|
c2x5dWsgPGtpc2x5dWtAZ21haWwuY29tPohoBBMRAgAoBQJXdAKuAhsDBQkSzAMA
|
|
BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRCK+vzSQoGKUgbWAJsFQ1PeMKGA
|
|
JC3GyQqoKIN65KibwQCfVWLmpSuP9Zmb9prjPDQyt8B0NXG0MkFuZHJleSBLaXNs
|
|
eXVrICh3ZWF2ZXIpIDxraXNseXVrQG9jZi5iZXJrZWxleS5lZHU+iGYEExECACYF
|
|
AkYdIyUCGwMFCRLMAwAGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCK+vzSQoGK
|
|
UiE9AKCADVzPhdseTQbrsVfZtqgIDqC/ZgCeL3UK2vHkLHBWiYzf1mCXzfgRpiG5
|
|
Ag0ERh0jKxAIAImRscnCUdzp52n8KLPsyD/U+hn5YAUfGef0n6+eUuEh1ygH0J+H
|
|
8W72K16dReY9X05Htod699TpHCWhmAL9DhqbAbjRtJ5B7hoTq6IS/FWbKRnsW/x7
|
|
il/pY6s7P74J1y1NYTbDC+5spw3QCACieU8QdK54sOhUVgTWYC55mu1ycVLSYFjp
|
|
me7Q6nKpcEq67zHx0QLmjFEryarlL7wEBvB33KzeAQ4thif8SHn3UM5TPkGlMs0h
|
|
T/BFNYH+dMd703HS+rwv1/vs87PxhIb+QqoK9BwVbZdLEd12D1Amk/oX/hZgELUO
|
|
tBgMj5Zbk+Jl6AWp8RgRWnzV1JSu2Zj/1wsAAwYH/1DwGUhpu9Sg3DgeEqp4tyyB
|
|
qVUtTXNc6F3XVzeD3i7jahjdJAlMhi+rQrQNZXNVTMdHn7mec5Oi6urGR79eabFv
|
|
cENWDHKj6QkMo4PCIi+y7TSoLyywQLxxxsDz5FGC5eTwSzo9Cdm6nwWhmCwNR2XG
|
|
bb68/TVinfYCXyo+q6EfmOKDeBQxCTdiUkWnkMPPfw4VV2/7KPxuJWcJ+eENFlY1
|
|
mHONQdJneIGpnXnvIT4peBPIVH4Q8FmZOzxEBvZQPKXvE3rxZYpFQBUmTb5kCeBb
|
|
VDRHm4iUY2IR1VFk2ExCCnYS6ds4Alt5sy4jQGY9ttOAXDX9hUBUpomG6AAvafqI
|
|
TwQYEQIADwUCRh0jKwIbDAUJEswDAAAKCRCK+vzSQoGKUmKLAJ90lCzOl94ztnyT
|
|
dImfPudUs7wYFQCeOS0T+YvYajEA99RWUnx2IH1rFKK5Ay4EV3QCMxEIAME79anw
|
|
+LZGB/d240txYXggyLefLskCJaIZrRoqxflTJCu6Gs6tTR8J29HO+GCuGKMMpLeF
|
|
x9uSMU2VeO+3HlpW9MD4EPGPL1ZZGecftXwFTMLCcoiPkLGqMzidENgQpNTyKJpE
|
|
vX1F3gIU5O53F35xeCWYSwNNAY9V+knNXG9syN59hrwa3Avr0Pu39lAABPKqKpqJ
|
|
LLMtxPSDfJljaEpbeTr0SS18cGO5ytK+sH7jBhp5oBi79tcqV3zpDCh1crSfZewb
|
|
Ugldem19fx3bRP7okYxSSJrDGjgte9jQWejeHRrUlSI9W8DEmVMo43CiEN6IG4aF
|
|
fYLs1MQK25nzc58BAKK5PWll/moC9Ne3sF2Deuw91gLNLEbwlQto4M49IMCRB/9h
|
|
9Ylaq+C4M3db46v/imwlSJmL7zVrbdENAY4aJEgpnSFNsgcFgnU0od+XTKiIVum+
|
|
Fxr6PbB2DcAoMqeGsu6R+uAWFJiAMyqLrAkcMeYg3p6DxY17oCLcIiEm9hnlA0K8
|
|
mYOw7Ykkb+oJqjhBUm5PFQH5KmXp/fbBuRiYrDfI/hBMw7J6XouH3vvrnIR5gBJM
|
|
UvFkFt9RDMFQ2g+PjHbvZx1RMNMRJdw2uqAl+HHkF2a/Piyf+U6lBnKnV161FPbS
|
|
mGKgbguN6y5SDfuDYHBH+GOmuYYDiU+BQnPhp0+fmEb2xZS4EzM7vcoCUzOAIbdl
|
|
EV4v/tduwyLcWX+J3sIfCACxxmKDR1aKhdG6BHY2UmYIsp54xo/MOJmco2DlQUpj
|
|
c45J8/4XYspXt5rI8fby81vHD2qMiDJ4aEYjDkmW+6XhuYjyr4xTqkvS9gMDggqH
|
|
EccGqz7IoWrQPWewDGxVINq9rLU0l6puKR6aXwx8J9RwRYy0oECjSdETuP+NdxXd
|
|
OVvnvBYMKSWWbIam1sjIiduczyVVRv8t1M5EKMG+KR/VS+hyjNZYduSp7OmettWT
|
|
wyIG3LXiBHffFHPZ0kn3P/sZ8inBEzsjfiVm9mLL3yIL2C1YSY2uaiigPBPeNJM3
|
|
QSnY6rSpfyGVKTC4/O9jX/+G0DJ7k0fdb7JwIS6Kd0huiK8EGBECAA8FAld0AjMC
|
|
GwIFCRLMAwAAagkQivr80kKBilJfIAQZEQgABgUCV3QCMwAKCRD51uveZ3aeBPHd
|
|
AP0WPpdXXqw2/kjuxydswKsI7I07uZlWeyou9zpath+0YgD8CW3o79TDafTHnpbo
|
|
fX3HT+yO2BftAx+rzCymzllQLe+kcwCfeiLfkBj+ZyX9fhNT5YyiagmY5LoAn1TH
|
|
qA34waak3JSxv16lZC0TqIuD
|
|
=cHO/
|
|
-----END PGP PUBLIC KEY BLOCK-----
|
|
"""
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(buffer=True)
|