79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Match an ISO's Companion bytes to the independently qualified source APK."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def regular(path, limit=None):
|
|
path = Path(path)
|
|
info = path.lstat()
|
|
if not stat.S_ISREG(info.st_mode) or path.is_symlink():
|
|
raise ValueError(f'unsafe Companion file: {path.name}')
|
|
if limit is not None and info.st_size > limit:
|
|
raise ValueError(f'oversized Companion file: {path.name}')
|
|
return path
|
|
|
|
|
|
def digest(path):
|
|
with regular(path).open('rb') as stream:
|
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
|
|
|
|
def qualify(repo, version):
|
|
script = str(repo / 'scripts/release_payload.py')
|
|
source = subprocess.check_output(
|
|
[sys.executable, script, 'source', '--version', version, '--root', str(repo)],
|
|
cwd=repo, text=True).strip()
|
|
if not re.fullmatch(r'[0-9a-f]{40}', source):
|
|
raise ValueError('release source qualification returned no exact commit')
|
|
companion = json.loads(subprocess.check_output(
|
|
[sys.executable, script, 'companion', '--root', str(repo)], cwd=repo, text=True))
|
|
return source, companion
|
|
|
|
|
|
def check(web_ui, source, qualified):
|
|
if not re.fullmatch(r'[0-9a-f]{40}', source):
|
|
raise ValueError('qualified source commit invalid')
|
|
for key in ('sha256', 'certificate_sha256'):
|
|
if not re.fullmatch(r'[0-9a-f]{64}', qualified.get(key, '')):
|
|
raise ValueError(f'qualified Companion {key} missing')
|
|
if qualified.get('package_name') != 'com.archipelago.app.debug':
|
|
raise ValueError('qualified Companion package differs')
|
|
if (not isinstance(qualified.get('versionName'), str) or not qualified['versionName']
|
|
or type(qualified.get('versionCode')) is not int or qualified['versionCode'] < 1):
|
|
raise ValueError('qualified Companion version missing')
|
|
apk = regular(web_ui / 'packages/archipelago-companion.apk')
|
|
metadata = json.loads(regular(web_ui / 'packages/archipelago-companion.json', 4096).read_bytes())
|
|
if digest(apk) != qualified['sha256']:
|
|
raise ValueError('ISO Companion APK differs from signed source APK')
|
|
if (metadata.get('versionName') != qualified.get('versionName')
|
|
or metadata.get('versionCode') != qualified.get('versionCode')):
|
|
raise ValueError('ISO Companion metadata differs from qualified APK')
|
|
return {'source_commit': source, 'apk_sha256': qualified['sha256'],
|
|
'versionName': qualified['versionName'],
|
|
'versionCode': qualified['versionCode'],
|
|
'certificate_sha256': qualified['certificate_sha256']}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--repo', required=True, type=Path)
|
|
parser.add_argument('--web-ui', required=True, type=Path)
|
|
parser.add_argument('--version', required=True)
|
|
args = parser.parse_args()
|
|
source, companion = qualify(args.repo, args.version)
|
|
print(json.dumps(check(args.web_ui, source, companion), sort_keys=True))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except (OSError, ValueError, subprocess.CalledProcessError) as error:
|
|
raise SystemExit(f'FAIL: {error}')
|