2022-04-25 22:25:32 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import inspect
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import optparse
|
|
|
|
import os
|
|
|
|
import shutil
|
2022-05-23 23:08:16 +02:00
|
|
|
import subprocess
|
2022-04-25 22:25:32 +02:00
|
|
|
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)
|
|
|
|
|
2023-02-17 12:39:15 +01:00
|
|
|
from fdroidserver import apksigcopier, common, exception, signindex, update
|
2022-04-25 22:25:32 +02:00
|
|
|
from pathlib import Path
|
2022-05-17 12:57:44 +02:00
|
|
|
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
|
2022-04-25 22:25:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
2022-05-23 12:39:17 +02:00
|
|
|
def test_sign_index(self):
|
2022-04-25 22:25:32 +02:00
|
|
|
shutil.copy(str(self.basedir / 'repo/index-v1.json'), 'repo')
|
2022-05-23 12:39:17 +02:00
|
|
|
signindex.sign_index(str(self.repodir), 'index-v1.json')
|
2022-04-25 22:25:32 +02:00
|
|
|
self.assertTrue((self.repodir / 'index-v1.jar').exists())
|
2022-05-17 12:57:44 +02:00
|
|
|
self.assertTrue((self.repodir / 'index-v1.json').exists())
|
2022-04-25 22:25:32 +02:00
|
|
|
|
2022-05-23 12:39:17 +02:00
|
|
|
def test_sign_index_corrupt(self):
|
2022-04-25 22:25:32 +02:00
|
|
|
with open('repo/index-v1.json', 'w') as fp:
|
|
|
|
fp.write('corrupt JSON!')
|
|
|
|
with self.assertRaises(json.decoder.JSONDecodeError, msg='error on bad JSON'):
|
2022-05-23 12:39:17 +02:00
|
|
|
signindex.sign_index(str(self.repodir), 'index-v1.json')
|
2022-04-25 22:25:32 +02:00
|
|
|
|
2023-02-17 12:39:15 +01:00
|
|
|
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())
|
|
|
|
|
2022-05-17 12:57:44 +02:00
|
|
|
def test_signindex(self):
|
2022-05-25 09:44:39 +02:00
|
|
|
if common.find_apksigner({}) is None: # TODO remove me for buildserver-bullseye
|
|
|
|
self.skipTest('SKIPPING test_signindex, apksigner not installed!')
|
2022-05-17 12:57:44 +02:00
|
|
|
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 = []
|
2022-05-23 23:10:52 +02:00
|
|
|
for f in (
|
|
|
|
'entry.jar',
|
|
|
|
'entry.json',
|
|
|
|
'index-v1.jar',
|
|
|
|
'index-v1.json',
|
|
|
|
'index-v2.json',
|
|
|
|
'index.jar',
|
|
|
|
'index.xml',
|
|
|
|
):
|
2022-05-17 12:57:44 +02:00
|
|
|
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)
|
2022-05-23 23:08:16 +02:00
|
|
|
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'
|
jarsigner: allow weak signatures
openjdk-11 11.0.17 in Debian unstable fails to verify weak signatures:
jarsigner -verbose -strict -verify tests/signindex/guardianproject.jar
131 Fri Dec 02 20:10:00 CET 2016 META-INF/MANIFEST.MF
252 Fri Dec 02 20:10:04 CET 2016 META-INF/1.SF
2299 Fri Dec 02 20:10:04 CET 2016 META-INF/1.RSA
0 Fri Dec 02 20:09:58 CET 2016 META-INF/
m ? 48743 Fri Dec 02 20:09:58 CET 2016 index.xml
s = signature was verified
m = entry is listed in manifest
k = at least one certificate was found in keystore
? = unsigned entry
- Signed by "EMAILADDRESS=root@guardianproject.info, CN=guardianproject.info, O=Guardian Project, OU=FDroid Repo, L=New York, ST=New York, C=US"
Digest algorithm: SHA1 (disabled)
Signature algorithm: SHA1withRSA (disabled), 4096-bit key
WARNING: The jar will be treated as unsigned, because it is signed with a weak algorithm that is now disabled by the security property:
jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, DSA keySize < 1024, SHA1 denyAfter 2019-01-01, include jdk.disabled.namedCurves
2022-10-29 22:09:07 +02:00
|
|
|
common.verify_deprecated_jar_signature(f)
|
2022-05-23 23:08:16 +02:00
|
|
|
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'
|
jarsigner: allow weak signatures
openjdk-11 11.0.17 in Debian unstable fails to verify weak signatures:
jarsigner -verbose -strict -verify tests/signindex/guardianproject.jar
131 Fri Dec 02 20:10:00 CET 2016 META-INF/MANIFEST.MF
252 Fri Dec 02 20:10:04 CET 2016 META-INF/1.SF
2299 Fri Dec 02 20:10:04 CET 2016 META-INF/1.RSA
0 Fri Dec 02 20:09:58 CET 2016 META-INF/
m ? 48743 Fri Dec 02 20:09:58 CET 2016 index.xml
s = signature was verified
m = entry is listed in manifest
k = at least one certificate was found in keystore
? = unsigned entry
- Signed by "EMAILADDRESS=root@guardianproject.info, CN=guardianproject.info, O=Guardian Project, OU=FDroid Repo, L=New York, ST=New York, C=US"
Digest algorithm: SHA1 (disabled)
Signature algorithm: SHA1withRSA (disabled), 4096-bit key
WARNING: The jar will be treated as unsigned, because it is signed with a weak algorithm that is now disabled by the security property:
jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, DSA keySize < 1024, SHA1 denyAfter 2019-01-01, include jdk.disabled.namedCurves
2022-10-29 22:09:07 +02:00
|
|
|
common.verify_deprecated_jar_signature(f)
|
2022-05-23 23:08:16 +02:00
|
|
|
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'
|
jarsigner: allow weak signatures
openjdk-11 11.0.17 in Debian unstable fails to verify weak signatures:
jarsigner -verbose -strict -verify tests/signindex/guardianproject.jar
131 Fri Dec 02 20:10:00 CET 2016 META-INF/MANIFEST.MF
252 Fri Dec 02 20:10:04 CET 2016 META-INF/1.SF
2299 Fri Dec 02 20:10:04 CET 2016 META-INF/1.RSA
0 Fri Dec 02 20:09:58 CET 2016 META-INF/
m ? 48743 Fri Dec 02 20:09:58 CET 2016 index.xml
s = signature was verified
m = entry is listed in manifest
k = at least one certificate was found in keystore
? = unsigned entry
- Signed by "EMAILADDRESS=root@guardianproject.info, CN=guardianproject.info, O=Guardian Project, OU=FDroid Repo, L=New York, ST=New York, C=US"
Digest algorithm: SHA1 (disabled)
Signature algorithm: SHA1withRSA (disabled), 4096-bit key
WARNING: The jar will be treated as unsigned, because it is signed with a weak algorithm that is now disabled by the security property:
jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, DSA keySize < 1024, SHA1 denyAfter 2019-01-01, include jdk.disabled.namedCurves
2022-10-29 22:09:07 +02:00
|
|
|
common.verify_deprecated_jar_signature(f)
|
2022-05-23 23:08:16 +02:00
|
|
|
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)
|
2022-05-17 12:57:44 +02:00
|
|
|
|
2022-04-25 22:25:32 +02:00
|
|
|
|
|
|
|
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)
|