Files

93 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Verify Reelbox's signed ARM64 release APK without reading signing secrets."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import zipfile
def digest(path):
value = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
value.update(chunk)
return value.hexdigest()
def verified_signer(signature, expected_sha256=None):
signer = re.search(r'Signer #1 certificate SHA-256 digest: ([0-9a-fA-F]{64})\b', signature)
subject = re.search(r'^Signer #1 certificate DN: (.+)$', signature, re.M)
assert signer and subject, 'No release certificate identity'
assert 'Android Debug' not in subject.group(1), 'Debug certificate is not a release signer'
fingerprint = signer.group(1).lower()
if expected_sha256:
assert fingerprint == expected_sha256.lower(), 'Wrong release certificate fingerprint'
return fingerprint, subject.group(1)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('apk', type=Path)
parser.add_argument('--android-sdk', type=Path, default=Path(os.environ.get('ANDROID_HOME', '/root/developer-tools/android')))
parser.add_argument('--build-tools', default='36.0.0')
parser.add_argument('--baseline-apk', type=Path)
parser.add_argument('--expected-signer-sha256', help='Pin the authorized release certificate, including a replacement key')
parser.add_argument('--output-dir', type=Path, required=True)
args = parser.parse_args()
source_version = re.search(r'^version:\s*([^+\s]+)\+(\d+)\s*$',
(Path(__file__).resolve().parents[1] / 'pubspec.yaml').read_text(), re.M)
assert source_version, 'Source version must include an explicit release name and code'
expected_name, expected_code = source_version.groups()
args.output_dir.mkdir(parents=True, exist_ok=True)
tools = args.android_sdk / 'build-tools' / args.build_tools
def run(tool, *arguments):
result = subprocess.run([str(tools / tool), *map(str, arguments)], capture_output=True, text=True, check=True)
return result.stdout + result.stderr
badging = run('aapt', 'dump', 'badging', args.apk)
signature = run('apksigner', 'verify', '--verbose', '--print-certs', args.apk)
(args.output_dir / 'apk-badging.log').write_text(badging)
(args.output_dir / 'apk-signature.log').write_text(signature)
package = re.search(r"package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'", badging)
assert package and package.groups() == ('org.reelbox.app', expected_code, expected_name), 'Wrong package/version; never deliver a stale APK'
assert 'application-debuggable' not in badging, 'Debuggable APK'
minimum = re.search(r"^sdkVersion:'(\d+)'", badging, re.M)
target = re.search(r"^targetSdkVersion:'(\d+)'", badging, re.M)
assert minimum and minimum.group(1) == '24'
assert target and target.group(1) == '36'
signer_sha256, signer_subject = verified_signer(signature, args.expected_signer_sha256)
with zipfile.ZipFile(args.apk) as archive:
assert archive.testzip() is None, 'APK ZIP corruption'
names = archive.namelist()
abis = sorted({name.split('/')[1] for name in names if name.startswith('lib/') and name.endswith('.so')})
assert abis == ['arm64-v8a'], 'Unsupported or dependency-only ABI advertised'
for library in ['libapp.so', 'libflutter.so', 'libmpv.so']:
assert 'lib/arm64-v8a/' + library in names, 'Missing ' + library
assert 'assets/flutter_assets/kernel_blob.bin' not in names, 'Debug kernel found'
baseline_matches = None
if args.baseline_apk:
baseline = run('apksigner', 'verify', '--print-certs', args.baseline_apk)
old = re.search(r'Signer #1 certificate SHA-256 digest: ([0-9a-fA-F]+)', baseline)
assert old and old.group(1).lower() == signer_sha256, 'Cannot upgrade the delivered beta: signer mismatch'
baseline_matches = True
report = {
'passed': True, 'apk': str(args.apk.resolve()), 'bytes': args.apk.stat().st_size,
'sha256': digest(args.apk), 'package': package.group(1), 'version_code': int(package.group(2)),
'version_name': package.group(3), 'min_sdk': 24, 'target_sdk': 36, 'abis': abis,
'release_signer': signer_subject, 'signer_sha256': signer_sha256,
'same_signer_as_delivered_beta': baseline_matches, 'zip_integrity': True,
'core_native_libraries_present': True, 'debuggable': False,
'android_hardware_verified': False,
}
(args.output_dir / 'apk-verification.json').write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
if __name__ == '__main__':
main()