#!/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}')