fdroidserver/tests/test_signindex.py
SilentGhost 7ff32bc4b0 Refactor TestCase files into python modules
Convert all TestCase files into standard python modules to be run and
discovered by unittest.
2024-11-20 10:37:52 +01:00

179 lines
6.9 KiB
Python
Executable File

#!/usr/bin/env python3
import json
import os
import shutil
import subprocess
import tempfile
import unittest
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()
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
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)