Files
archy/scripts/iso_companion_check.py

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}')