#!/usr/bin/env python3 import inspect import json import logging import optparse import os import shutil import subprocess import sys import tempfile import unittest localmodule = os.path.realpath( os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..') ) print('localmodule: ' + localmodule) if localmodule not in sys.path: sys.path.insert(0, localmodule) from fdroidserver import apksigcopier, common, exception, signindex, update from pathlib import Path from unittest.mock import patch class Options: allow_disabled_algorithms = False clean = False delete_unknown = False nosign = False pretty = True rename_apks = False verbose = False class SignindexTest(unittest.TestCase): basedir = Path(__file__).resolve().parent def setUp(self): signindex.config = None config = common.read_config(common.options) config['jarsigner'] = common.find_sdk_tools_cmd('jarsigner') config['verbose'] = True config['keystore'] = str(self.basedir / 'keystore.jks') config['repo_keyalias'] = 'sova' config['keystorepass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=' config['keypass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI=' signindex.config = config logging.basicConfig(level=logging.DEBUG) self.tempdir = tempfile.TemporaryDirectory() os.chdir(self.tempdir.name) self.repodir = Path('repo') self.repodir.mkdir() def tearDown(self): self.tempdir.cleanup() def test_sign_index(self): shutil.copy(str(self.basedir / 'repo/index-v1.json'), 'repo') signindex.sign_index(str(self.repodir), 'index-v1.json') self.assertTrue((self.repodir / 'index-v1.jar').exists()) self.assertTrue((self.repodir / 'index-v1.json').exists()) def test_sign_index_corrupt(self): with open('repo/index-v1.json', 'w') as fp: fp.write('corrupt JSON!') with self.assertRaises(json.decoder.JSONDecodeError, msg='error on bad JSON'): signindex.sign_index(str(self.repodir), 'index-v1.json') def test_sign_entry(self): entry = 'repo/entry.json' v2 = 'repo/index-v2.json' shutil.copy(self.basedir / entry, entry) shutil.copy(self.basedir / v2, v2) signindex.sign_index(self.repodir, 'entry.json') self.assertTrue((self.repodir / 'entry.jar').exists()) def test_sign_entry_corrupt(self): """sign_index should exit with error if entry.json is bad JSON""" entry = 'repo/entry.json' with open(entry, 'w') as fp: fp.write('{') with self.assertRaises(json.decoder.JSONDecodeError, msg='error on bad JSON'): signindex.sign_index(self.repodir, 'entry.json') self.assertFalse((self.repodir / 'entry.jar').exists()) def test_sign_entry_corrupt_leave_entry_jar(self): """sign_index should not touch existing entry.jar if entry.json is corrupt""" existing = 'repo/entry.jar' testvalue = "Don't touch!" with open(existing, 'w') as fp: fp.write(testvalue) with open('repo/entry.json', 'w') as fp: fp.write('{') with self.assertRaises(json.decoder.JSONDecodeError, msg='error on bad JSON'): signindex.sign_index(self.repodir, 'entry.json') with open(existing) as fp: self.assertEqual(testvalue, fp.read()) def test_sign_corrupt_index_v2_json(self): """sign_index should exit with error if index-v2.json JSON is corrupt""" with open('repo/index-v2.json', 'w') as fp: fp.write('{"key": "not really an index"') good_entry = { "timestamp": 1676583021000, "version": 20002, "index": { "name": "/index-v2.json", "sha256": common.sha256sum('repo/index-v2.json'), "size": os.path.getsize('repo/index-v2.json'), "numPackages": 0, }, } with open('repo/entry.json', 'w') as fp: json.dump(good_entry, fp) with self.assertRaises(json.decoder.JSONDecodeError, msg='error on bad JSON'): signindex.sign_index(self.repodir, 'entry.json') self.assertFalse((self.repodir / 'entry.jar').exists()) def test_sign_index_v2_corrupt_sha256(self): """sign_index should exit with error if SHA-256 of file in entry is wrong""" entry = 'repo/entry.json' v2 = 'repo/index-v2.json' shutil.copy(self.basedir / entry, entry) shutil.copy(self.basedir / v2, v2) with open(v2, 'a') as fp: fp.write(' ') with self.assertRaises(exception.FDroidException, msg='error on bad SHA-256'): signindex.sign_index(self.repodir, 'entry.json') self.assertFalse((self.repodir / 'entry.jar').exists()) def test_signindex(self): if common.find_apksigner({}) is None: # TODO remove me for buildserver-bullseye self.skipTest('SKIPPING test_signindex, apksigner not installed!') os.mkdir('archive') metadata = Path('metadata') metadata.mkdir() with (metadata / 'info.guardianproject.urzip.yml').open('w') as fp: fp.write('# placeholder') shutil.copy(str(self.basedir / 'urzip.apk'), 'repo') index_files = [] for f in ( 'entry.jar', 'entry.json', 'index-v1.jar', 'index-v1.json', 'index-v2.json', 'index.jar', 'index.xml', ): for section in (Path('repo'), Path('archive')): path = section / f self.assertFalse(path.exists(), '%s should not exist yet!' % path) index_files.append(path) common.options = Options with patch('sys.argv', ['fdroid update']): update.main() with patch('sys.argv', ['fdroid signindex', '--verbose']): signindex.main() for f in index_files: self.assertTrue(f.exists(), '%s should exist!' % f) self.assertFalse(os.path.exists('index-v2.jar')) # no JAR version of this file # index.jar aka v0 must by signed by SHA1withRSA f = 'repo/index.jar' common.verify_deprecated_jar_signature(f) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) cp = subprocess.run( ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ) self.assertTrue(b'SHA1withRSA' in cp.stdout) # index-v1.jar must by signed by SHA1withRSA f = 'repo/index-v1.jar' common.verify_deprecated_jar_signature(f) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) cp = subprocess.run( ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ) self.assertTrue(b'SHA1withRSA' in cp.stdout) # entry.jar aka index v2 must by signed by a modern algorithm f = 'repo/entry.jar' common.verify_deprecated_jar_signature(f) self.assertIsNone(apksigcopier.extract_v2_sig(f, expected=False)) cp = subprocess.run( ['jarsigner', '-verify', '-verbose', f], stdout=subprocess.PIPE ) self.assertFalse(b'SHA1withRSA' in cp.stdout) if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) parser = optparse.OptionParser() parser.add_option( "-v", "--verbose", action="store_true", default=False, help="Spew out even more information than normal", ) (common.options, args) = parser.parse_args(['--verbose']) newSuite = unittest.TestSuite() newSuite.addTest(unittest.makeSuite(SignindexTest)) unittest.main(failfast=False)