SHA256
Verify the production manifest, system-only trust, development APK signature and unsigned release outputs before collecting local artifacts. Record source identity and hashes outside Git. Exercised successfully against the built APKs and AAB; collection must follow a clean-source build.
69 lines
3.5 KiB
Python
69 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Collect development/unsigned outputs with source identity and inspection evidence."""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import zipfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BUILD = Path(os.environ.get("YUVOMI_BUILD_DIR", ROOT / "android"))
|
|
APP = BUILD / "app" if "YUVOMI_BUILD_DIR" in os.environ else BUILD / "app/build"
|
|
SDK = Path(os.environ["ANDROID_HOME"])
|
|
TOOLS = SDK / "build-tools/36.0.0"
|
|
DESTINATION = ROOT / "artifacts/android"
|
|
DESTINATION.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def tool(name, *arguments):
|
|
return subprocess.check_output([str(TOOLS / name), *map(str, arguments)], text=True)
|
|
|
|
|
|
release = APP / "outputs/apk/release/app-release-unsigned.apk"
|
|
manifest = tool("aapt2", "dump", "xmltree", release, "--file", "AndroidManifest.xml")
|
|
assert 'package="community.yuvomi.mobile"' in manifest
|
|
assert re.search(r":allowBackup\([^\n]*=false", manifest)
|
|
assert re.search(r":usesCleartextTraffic\([^\n]*=false", manifest)
|
|
assert not re.search(r":debuggable\([^\n]*=true", manifest)
|
|
resources = tool("aapt2", "dump", "resources", release)
|
|
network = re.search(r"xml/network_security_config\s+\(\) \(file\) (\S+)", resources)
|
|
assert network, "Missing production network security configuration"
|
|
policy = tool("aapt2", "dump", "xmltree", release, "--file", network.group(1))
|
|
assert "cleartextTrafficPermitted=false" in policy
|
|
assert policy.count("E: certificates") == 1 and 'src="system"' in policy
|
|
(DESTINATION / "release-manifest.txt").write_text(manifest)
|
|
(DESTINATION / "release-network-policy.txt").write_text(policy)
|
|
|
|
debug = APP / "outputs/apk/debug/app-debug.apk"
|
|
(DESTINATION / "debug-signature.txt").write_text(tool("apksigner", "verify", "--verbose", debug))
|
|
unsigned = subprocess.run([str(TOOLS / "apksigner"), "verify", str(release)], capture_output=True, text=True)
|
|
assert unsigned.returncode != 0 and "Missing META-INF/MANIFEST.MF" in unsigned.stderr, unsigned.stderr
|
|
|
|
entries = []
|
|
for source in [debug, release, APP / "outputs/bundle/release/app-release.aab"]:
|
|
with zipfile.ZipFile(source) as archive:
|
|
for name in archive.namelist():
|
|
assert "integration_ca" not in name and not name.endswith((".pem", ".jks", ".keystore", ".p12")), name
|
|
if source.suffix == ".aab":
|
|
assert not any(name.upper().startswith("META-INF/") and name.upper().endswith((".RSA", ".DSA", ".EC"))
|
|
for name in archive.namelist()), "Expected unsigned AAB"
|
|
destination = DESTINATION / source.name
|
|
shutil.copyfile(source, destination)
|
|
entries.append({"file": source.name, "bytes": destination.stat().st_size,
|
|
"sha256": hashlib.sha256(destination.read_bytes()).hexdigest()})
|
|
|
|
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
|
|
dirty = bool(subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True).strip())
|
|
(DESTINATION / "SHA256SUMS").write_text("".join(f"{entry['sha256']} {entry['file']}\n" for entry in entries))
|
|
(DESTINATION / "build-info.json").write_text(json.dumps({
|
|
"source_revision": revision, "worktree_dirty_at_collection": dirty,
|
|
"upstream_revision": "10bfef7686ed7303b4c74c7fcc55a5eb16356c9d",
|
|
"status": "Development debug APK; unsigned release APK/AAB. Not production-ready.",
|
|
"artifacts": entries,
|
|
}, indent=2) + "\n")
|
|
print(f"Inspected and collected {len(entries)} artifacts in {DESTINATION}")
|
|
print("Build source identity assumes the documented build ran immediately before collection.")
|