93 lines
4.7 KiB
Python
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()
|