123456789101112131415161718192021from hashlib import sha25622from pathlib import Path23import re24import sys25import zipfile2627ROOT = Path(__file__).resolve().parents[1]28INVENTORY = ROOT / "docs" / "android-native-provenance-inventory.txt"29PROVENANCE = ROOT / "docs" / "NOISE-CORE-PROVENANCE.md"30JNI_LIBS = ROOT / "app" / "src" / "main" / "jniLibs"31NOISE_CORE = "libpeach_noise_core.so"323334def digest_bytes(data: bytes) -> str:35 return sha256(data).hexdigest()363738def digest_file(path: Path) -> str:39 hasher = sha256()40 with path.open("rb") as stream:41 for chunk in iter(lambda: stream.read(1024 * 1024), b""):42 hasher.update(chunk)43 return hasher.hexdigest()444546def inventory_records() -> dict[str, str]:47 if not INVENTORY.is_file():48 raise SystemExit(f"checked-in native inventory not found: {INVENTORY}")49 records = {}50 for line in INVENTORY.read_text().splitlines():51 if not line.strip() or line.startswith("#"):52 continue53 try:54 path, expected = line.split("|", 1)55 except ValueError:56 raise SystemExit(f"invalid native inventory line: {line}")57 records[path.strip()] = expected.strip()58 return records596061def abi_of(packaged_path: str) -> str:62 return Path(packaged_path).parent.name636465records = inventory_records()66if not records:67 raise SystemExit("native inventory is empty")686970if not JNI_LIBS.is_dir():71 raise SystemExit(f"native source set not found: {JNI_LIBS}")72source_set = {}73for path in sorted(JNI_LIBS.rglob("*.so")):74 if not path.is_file():75 continue76 packaged = f"lib/{path.parent.name}/{path.name}"77 source_set[packaged] = digest_file(path)7879unrecorded_source = sorted(set(source_set) - set(records))80if unrecorded_source:81 raise SystemExit(82 "native source set contains unrecorded libraries: " + ", ".join(unrecorded_source)83 )84for packaged, actual in source_set.items():85 if records[packaged] != actual:86 raise SystemExit(87 f"committed native library digest mismatch for {packaged}: "88 f"recorded {records[packaged]}, actual {actual}"89 )90919293if len(sys.argv) > 1:94 apk = Path(sys.argv[1])95 if not apk.is_file():96 raise SystemExit(f"release APK not found: {apk}")97 with zipfile.ZipFile(apk) as archive:98 packaged = {99 name: digest_bytes(archive.read(name))100 for name in archive.namelist()101 if name.startswith("lib/") and name.endswith(".so")102 }103 if not packaged:104 raise SystemExit("release APK contains no native libraries")105 shipped_abis = {abi_of(name) for name in packaged}106 expected = {name for name in records if abi_of(name) in shipped_abis}107 unrecorded = sorted(set(packaged) - set(records))108 missing = sorted(expected - set(packaged))109 if unrecorded or missing:110 raise SystemExit(111 "packaged native libraries do not match the inventory: "112 f"packaged but unrecorded {unrecorded}; recorded but absent {missing}"113 )114 for name, actual in packaged.items():115 if records[name] != actual:116 raise SystemExit(117 f"packaged native library digest mismatch for {name}: "118 f"recorded {records[name]}, actual {actual}"119 )120121122provenance = PROVENANCE.read_text()123for packaged, expected in records.items():124 if Path(packaged).name != NOISE_CORE:125 continue126 abi = abi_of(packaged)127 match = re.search(rf"^- `{re.escape(abi)}`: `([0-9a-f]+)`$", provenance, re.MULTILINE)128 if match is None:129 raise SystemExit(f"provenance document has no authoritative hash for {abi}")130 if match.group(1) != expected:131 raise SystemExit(132 f"provenance digest mismatch for {packaged}: "133 f"recorded {expected}, documented {match.group(1)}"134 )135136print(f"PASS: native provenance verified ({len(records)} packaged libraries)")