ffi_source.py (13564B)
1 """Bounded source capture for the application-owned native producer.""" 2 3 from __future__ import annotations 4 5 import hashlib 6 import os 7 import re 8 import stat 9 import subprocess 10 from pathlib import Path 11 from typing import Any 12 13 import package_contract as contract 14 15 MAX_BYTES = 2 * 1024 * 1024 16 MAX_INPUTS = 2048 17 INPUTS = [ 18 "test-fixtures/legacy-identifiers.v1.json", 19 "Cargo.toml", 20 "Cargo.lock", 21 "rust-toolchain.toml", 22 "Makefile", 23 "core", 24 "scripts", 25 "TeraFFI/Makefile", 26 "TeraFFI/scripts", 27 "TeraFFI/producer.toml", 28 "radroots.lib.source-lock.v1.toml", 29 ] 30 PRODUCER_PATH = "TeraFFI/producer.toml" 31 32 33 class ProvenanceError(Exception): 34 """A source-free, fail-closed provenance rejection.""" 35 36 37 def command(root: Path, argv: list[str], data: bytes | None = None) -> bytes: 38 if argv[0] == "git": 39 argv = ["git", "--no-replace-objects", *argv[1:]] 40 try: 41 result = subprocess.run( 42 argv, cwd=root, input=data, capture_output=True, check=False, timeout=120 43 ) 44 except (OSError, subprocess.TimeoutExpired) as error: 45 raise ProvenanceError("producer inspection command unavailable") from error 46 if result.returncode or len(result.stdout) > MAX_BYTES: 47 raise ProvenanceError("producer inspection command failed or exceeded bound") 48 return result.stdout 49 50 51 def read_source(root: Path, relative: str) -> bytes: 52 path = Path(relative) 53 if path.is_absolute() or str(path) != relative or ".." in path.parts: 54 raise ProvenanceError("producer input path is invalid") 55 current = root 56 for part in path.parts: 57 current = current / part 58 if current.is_symlink(): 59 raise ProvenanceError("producer input contains a symlink") 60 return contract._read_regular(current, maximum=MAX_BYTES) 61 62 63 def producer_contract(root: Path) -> dict[str, Any]: 64 value = contract._read_toml(root / PRODUCER_PATH) 65 expected = { 66 "schema", 67 "repository", 68 "foundation_lock", 69 "source_inputs", 70 "ffi", 71 "generator", 72 "build", 73 } 74 contract._exact(set(value), expected, "producer contract fields") 75 contract._exact(value["schema"], "tera.native-producer.v1", "producer schema") 76 contract._exact( 77 value["repository"], 78 "https://github.com/radrootslabs/tera", 79 "producer repository", 80 ) 81 contract._exact(value["source_inputs"], INPUTS, "producer input inventory") 82 contract._exact( 83 value["foundation_lock"], 84 "radroots.lib.source-lock.v1.toml", 85 "foundation lock path", 86 ) 87 contract._exact( 88 value["ffi"], 89 { 90 "package": "tera_ffi", 91 "default_features": True, 92 "features": [], 93 "config": "core/crates/tera_ffi/uniffi.toml", 94 }, 95 "FFI producer selection", 96 ) 97 contract._exact( 98 value["generator"], 99 { 100 "package": "tera_bindgen", 101 "default_features": True, 102 "features": [], 103 }, 104 "generator selection", 105 ) 106 validate_build(value["build"]) 107 cargo = contract._read_toml(root / "Cargo.toml") 108 lock = contract._read_toml(root / value["foundation_lock"]) 109 validate_foundation(cargo, lock) 110 contract._exact( 111 lock["lockfile_sha256"], 112 hashlib.sha256(read_source(root, "Cargo.lock")).hexdigest(), 113 "foundation consumer Cargo lock digest", 114 ) 115 toolchain = contract._read_toml(root / "rust-toolchain.toml") 116 contract._exact( 117 toolchain, 118 { 119 "toolchain": { 120 "channel": value["build"]["rust_version"], 121 "profile": "minimal", 122 "components": ["llvm-tools"], 123 } 124 }, 125 "producer toolchain", 126 ) 127 return value 128 129 130 def validate_build(value: Any) -> None: 131 value = contract._mapping(value, "producer build") 132 contract._exact( 133 set(value), 134 { 135 "rust_version", 136 "profile", 137 "ios_deployment_target", 138 "rust_flags", 139 "source_date_epoch", 140 "host", 141 "host_dylib_install_name", 142 "host_linker_reproducible", 143 "host_oso_prefix", 144 "targets", 145 }, 146 "producer build fields", 147 ) 148 contract._exact(value["rust_version"], "1.97.1", "producer Rust version") 149 contract._exact(value["profile"], "release", "producer build profile") 150 contract._exact( 151 value["ios_deployment_target"], "18.0", "producer deployment target" 152 ) 153 contract._exact(value["host"], "aarch64-apple-darwin", "producer host") 154 contract._exact( 155 value["host_dylib_install_name"], 156 "@rpath/libtera_ffi.dylib", 157 "producer host dylib install name", 158 ) 159 contract._exact( 160 value["host_linker_reproducible"] is True, 161 True, 162 "producer host linker reproducibility", 163 ) 164 contract._exact( 165 value["host_oso_prefix"], "{extbuild_root}", "producer host debug-map prefix" 166 ) 167 contract._exact( 168 value["rust_flags"], 169 [ 170 "--remap-path-prefix={producer_root}=/tera", 171 "--remap-path-prefix={cargo_home}=/cargo", 172 "--remap-path-prefix={extbuild_root}=/build", 173 ], 174 "producer Rust flags", 175 ) 176 contract._exact( 177 value["targets"], 178 [ 179 "aarch64-apple-ios", 180 "aarch64-apple-ios-sim", 181 "aarch64-apple-darwin", 182 ], 183 "producer targets", 184 ) 185 if type(value["source_date_epoch"]) is not int or value["source_date_epoch"] <= 0: 186 raise ProvenanceError("producer source epoch is invalid") 187 188 189 def library_rust_flags(build: dict[str, Any], target: str) -> list[str]: 190 if target == build["host"]: 191 return [ 192 f"-Clink-arg=-Wl,-install_name,{build['host_dylib_install_name']}", 193 "-Clink-arg=-Wl,-reproducible", 194 f"-Clink-arg=-Wl,-oso_prefix,{build['host_oso_prefix']}", 195 ] 196 return [] 197 198 199 def validate_foundation(cargo: dict[str, Any], lock: dict[str, Any]) -> None: 200 contract._exact( 201 lock.get("schema"), "radroots.lib.source-lock.v1", "foundation schema" 202 ) 203 contract._exact(lock.get("repository"), contract.LIB_REMOTE, "foundation remote") 204 if not contract.GIT_REVISION.fullmatch(str(lock.get("revision", ""))): 205 raise ProvenanceError("foundation revision is invalid") 206 for field in ( 207 "workspace_catalog_sha256", 208 "source_archive_sha256", 209 "lockfile_sha256", 210 ): 211 if not contract.SHA256.fullmatch(str(lock.get(field, ""))): 212 raise ProvenanceError("foundation digest is invalid") 213 dependencies = cargo["workspace"]["dependencies"] 214 selected = { 215 name: value 216 for name, value in dependencies.items() 217 if name.startswith("radroots_") 218 } 219 if not selected: 220 raise ProvenanceError("foundation dependency inventory is empty") 221 for value in selected.values(): 222 value = contract._mapping(value, "foundation Cargo dependency") 223 contract._exact(value.get("git"), lock["repository"], "foundation Cargo remote") 224 contract._exact(value.get("rev"), lock["revision"], "foundation Cargo revision") 225 contract._exact( 226 value.get("version"), "=" + lock["version"], "foundation Cargo version" 227 ) 228 229 230 def source_snapshot(root: Path, inputs: list[str]) -> dict[str, Any]: 231 actual_root = ( 232 command(root, ["git", "rev-parse", "--show-toplevel"]).decode().strip() 233 ) 234 if actual_root != str(root): 235 raise ProvenanceError("producer source must use its own repository root") 236 if command( 237 root, ["git", "ls-files", "--others", "--exclude-standard", "-z", "--", *inputs] 238 ): 239 raise ProvenanceError("producer inputs contain untracked source") 240 reject_ignored_source(root, inputs) 241 raw = command(root, ["git", "ls-files", "--stage", "-z", "--", *inputs]) 242 rows = raw.rstrip(b"\0").split(b"\0") if raw else [] 243 if not rows or len(rows) > MAX_INPUTS: 244 raise ProvenanceError("producer input inventory exceeds bounds") 245 files: dict[str, Any] = {} 246 tree: dict[str, Any] = {} 247 for row in rows: 248 relative, entry = staged_file(root, row) 249 files[relative] = entry 250 insert_tree(tree, relative, entry["mode"], entry["git_blob"]) 251 require_input_inventory(inputs, files) 252 return {"policy": "staged_inputs", "tree": tree_identity(tree), "files": files} 253 254 255 def require_input_inventory(inputs: list[str], files: dict[str, Any]) -> None: 256 for relative in inputs: 257 if not any( 258 name == relative or name.startswith(relative + "/") for name in files 259 ): 260 raise ProvenanceError("required producer input is missing from index") 261 262 263 def reject_ignored_source(root: Path, inputs: list[str]) -> None: 264 ignored = command( 265 root, 266 [ 267 "git", 268 "ls-files", 269 "--others", 270 "--ignored", 271 "--exclude-standard", 272 "-z", 273 "--", 274 *inputs, 275 ":(exclude)scripts/persona-verifier/.venv/**", 276 ":(exclude,glob)**/__pycache__/**", 277 ], 278 ) 279 if any(is_input(path, inputs) for path in ignored.decode().split("\0") if path): 280 raise ProvenanceError( 281 "producer inputs contain ignored source outside tool caches" 282 ) 283 284 285 def is_input(relative: str, inputs: list[str]) -> bool: 286 return any(relative == name or relative.startswith(name + "/") for name in inputs) 287 288 289 def staged_file(root: Path, row: bytes) -> tuple[str, dict[str, Any]]: 290 header, encoded_path = row.split(b"\t", 1) 291 mode, oid, stage = header.decode("ascii").split() 292 relative = encoded_path.decode("utf-8") 293 if stage != "0" or mode not in ("100644", "100755"): 294 raise ProvenanceError("producer input has unresolved or unsupported mode") 295 data = read_source(root, relative) 296 blob = command(root, ["git", "cat-file", "blob", oid]) 297 actual_mode = ( 298 "100755" if (root / relative).stat().st_mode & stat.S_IXUSR else "100644" 299 ) 300 if data != blob or mode != actual_mode: 301 raise ProvenanceError("producer worktree differs from staged source") 302 return relative, { 303 "mode": mode, 304 "git_blob": oid, 305 "bytes": len(data), 306 "sha256": hashlib.sha256(data).hexdigest(), 307 } 308 309 310 def reject_cargo_configuration(root: Path) -> None: 311 locations = [path / ".cargo" for path in (root, *root.parents)] 312 locations.append(Path(os.environ.get("CARGO_HOME", str(Path.home() / ".cargo")))) 313 for location in locations: 314 for name in ("config", "config.toml"): 315 path = location / name 316 if path.exists(): 317 value = contract._read_toml(path) 318 if {"build", "target", "env", "profile", "patch", "unstable"} & set( 319 value 320 ): 321 raise ProvenanceError( 322 "ungoverned Cargo configuration affects producer build" 323 ) 324 325 326 def insert_tree(tree: dict[str, Any], relative: str, mode: str, oid: str) -> None: 327 parts = relative.split("/") 328 for part in parts[:-1]: 329 tree = tree.setdefault(part, {}) 330 tree[parts[-1]] = (mode, oid) 331 332 333 def tree_identity(tree: dict[str, Any]) -> str: 334 rows = [] 335 for name, value in tree.items(): 336 if isinstance(value, dict): 337 mode, oid, key = "40000", tree_identity(value), name.encode() + b"/" 338 else: 339 mode, oid = value 340 key = name.encode() 341 rows.append((key, f"{mode} {name}".encode() + b"\0" + bytes.fromhex(oid))) 342 body = b"".join(row for _, row in sorted(rows)) 343 return hashlib.sha1( 344 b"tree " + str(len(body)).encode() + b"\0" + body, usedforsecurity=False 345 ).hexdigest() 346 347 348 def reject_build_overrides() -> None: 349 forbidden = ( 350 "RUSTFLAGS", 351 "RUSTC", 352 "RUSTC_WRAPPER", 353 "RUSTC_WORKSPACE_WRAPPER", 354 "CARGO_ENCODED_RUSTFLAGS", 355 "GIT_INDEX_FILE", 356 "GIT_DIR", 357 "GIT_WORK_TREE", 358 "GIT_OBJECT_DIRECTORY", 359 "GIT_ALTERNATE_OBJECT_DIRECTORIES", 360 "GIT_CONFIG_COUNT", 361 "GIT_CONFIG_PARAMETERS", 362 ) 363 if any(os.environ.get(name) for name in forbidden): 364 raise ProvenanceError("ungoverned Rust build override is active") 365 if any( 366 re.fullmatch( 367 r"CARGO_(BUILD_.*|PROFILE_.*|TARGET_.*_(RUSTFLAGS|LINKER|RUNNER))", name 368 ) 369 and name not in allowed_profile_overrides() 370 for name in os.environ 371 ): 372 raise ProvenanceError("ungoverned Cargo build override is active") 373 374 375 def allowed_profile_overrides() -> dict[str, str]: 376 # Extbuild's development debug policy affects the generator, not release libraries. 377 name = "CARGO_PROFILE_DEV_DEBUG" 378 if ( 379 os.environ.get("EXT_BUILD_RUN_ACTIVE") 380 and os.environ.get(name) == "line-tables-only" 381 ): 382 return {name: "line-tables-only"} 383 return {} 384 385 386 def feature_graph(root: Path, package: str, target: str) -> list[str]: 387 raw = command( 388 root, 389 [ 390 "cargo", 391 "tree", 392 "--locked", 393 "--offline", 394 "-p", 395 package, 396 "--target", 397 target, 398 "--edges", 399 "normal,build", 400 "--prefix", 401 "none", 402 "--format", 403 "{p}|{f}", 404 ], 405 ).decode() 406 return sorted(set(raw.replace(str(root), "<producer-root>").splitlines()))