#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-only """Offline, bounded notice assembly for the reviewed installed dependency snapshot.""" import argparse from contextlib import contextmanager import hashlib import json import os from pathlib import Path, PurePosixPath import re import stat import sys CATALOG = "third_party/release-notices/inputs.json" MAX_FILE = 4 * 1024 * 1024 MAX_TOTAL = 32 * 1024 * 1024 MAX_ENTRIES = 128 ROOTS = {"project", "sdk", "toolchain"} DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC FILE_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC INTRO = """Release notice bundle — scoped engineering aid, NOT legal clearance Start with inputs/project/third_party/release-notices/README.md. manifest.json records logical input paths, exact source hashes, byte ranges, and output hashes. No host paths or timestamps are recorded. Texts are retained verbatim, including mixed grants and the wolfSSH package/header discrepancy. This is NOT corresponding source, a complete SBOM, an archive of the SDK/tools, or proof of notice delivery to firmware/device/browser recipients. Radio-blob corresponding-source/System Library questions remain unresolved. Exact icon SVG source evidence and manual-derivative limits are in inputs/project/docs/icon_provenance.md. WolfSSH packaging clarification, bootloader/runtime attribution, source delivery and Installation Information still require release review. No legal clearance. """.encode("utf-8") class NoticeError(Exception): """A failed precondition; never silently omit a required input.""" def digest(data): return hashlib.sha256(data).hexdigest() def json_bytes(value): return (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode("utf-8") def relative_parts(value): if not isinstance(value, str) or not value or "\\" in value or "\x00" in value: raise NoticeError("invalid relative input path") parts = value.split("/") if PurePosixPath(value).is_absolute() or any(p in ("", ".", "..") for p in parts): raise NoticeError("unsafe relative input path") return parts def absolute_path(value): path = Path(value) if ".." in path.parts: raise NoticeError("parent traversal is not allowed") # Do not resolve(): it would hide symlinks from the descriptor walk. return Path(os.path.abspath(path)) @contextmanager def directory(path): """Pin every directory component, rejecting symlinks (including ancestors).""" path = absolute_path(path) fd = os.open(path.anchor, DIR_FLAGS) try: for part in path.parts[1:]: child = os.open(part, DIR_FLAGS, dir_fd=fd) os.close(fd) fd = child yield fd finally: os.close(fd) def read_bounded(root_fd, path, limit=MAX_FILE): parts = relative_parts(path) fd = os.dup(root_fd) try: for part in parts[:-1]: child = os.open(part, DIR_FLAGS, dir_fd=fd) os.close(fd) fd = child source = os.open(parts[-1], FILE_FLAGS, dir_fd=fd) try: info = os.fstat(source) if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= limit: raise NoticeError(f"not a nonempty bounded regular file: {path}") with os.fdopen(source, "rb", closefd=False) as stream: data = stream.read(limit + 1) if len(data) != info.st_size or len(data) > limit: raise NoticeError(f"input changed size or exceeded limit: {path}") return data finally: os.close(source) finally: os.close(fd) def validate_catalog(catalog): if not isinstance(catalog, dict) or set(catalog) != {"schema", "snapshot", "inputs"}: raise NoticeError("invalid catalog structure") if catalog["schema"] != 1 or not isinstance(catalog["snapshot"], dict): raise NoticeError("unsupported catalog schema") entries = catalog["inputs"] if not isinstance(entries, list) or not 0 < len(entries) <= MAX_ENTRIES: raise NoticeError("invalid catalog input count") seen = set() total = 0 for entry in entries: if not isinstance(entry, dict) or set(entry) != { "root", "path", "size", "sha256", "range", "output_sha256", "purpose" }: raise NoticeError("invalid catalog entry") if entry["root"] not in ROOTS: raise NoticeError("unknown input root") relative_parts(entry["path"]) key = (entry["root"], entry["path"]) if key in seen: raise NoticeError("duplicate input") seen.add(key) size = entry["size"] if type(size) is not int or not 0 < size <= MAX_FILE: raise NoticeError("invalid input size") total += size for field in ("sha256", "output_sha256"): if not isinstance(entry[field], str) or not re.fullmatch(r"[0-9a-f]{64}", entry[field]): raise NoticeError("invalid digest") span = entry["range"] if span is not None and ( not isinstance(span, list) or len(span) != 2 or any(type(n) is not int for n in span) or not 0 <= span[0] < span[1] <= size ): raise NoticeError("invalid byte range") if not isinstance(entry["purpose"], str) or not entry["purpose"]: raise NoticeError("missing input purpose") if total > MAX_TOTAL: raise NoticeError("catalog exceeds total read budget") def assemble(roots, output, catalog_data): """Preflight all inputs before creating output. Catalog is trusted reviewed policy.""" if set(roots) != ROOTS: raise NoticeError("all three input roots are required") roots = {name: absolute_path(path) for name, path in roots.items()} output = absolute_path(output) for root in roots.values(): if output == root or root in output.parents: raise NoticeError("output must be outside every input root") if len(catalog_data) > MAX_FILE: raise NoticeError("catalog exceeds size bound") catalog = json.loads(catalog_data) validate_catalog(catalog) payloads = {} records = [] for entry in sorted(catalog["inputs"], key=lambda e: (e["root"], e["path"])): logical = f"{entry['root']}/{entry['path']}" try: with directory(roots[entry["root"]]) as root_fd: source = read_bounded(root_fd, entry["path"], entry["size"]) except OSError as error: raise NoticeError(f"cannot read required input: {logical} ({error.strerror})") from error if len(source) != entry["size"] or digest(source) != entry["sha256"]: raise NoticeError(f"source hash/size mismatch: {logical}; review drift, do not auto-repin") span = entry["range"] data = source if span is None else source[span[0]:span[1]] if digest(data) != entry["output_sha256"]: raise NoticeError(f"excerpt hash mismatch: {logical}") try: data.decode("utf-8") except UnicodeDecodeError as error: raise NoticeError(f"non-UTF-8 notice: {logical}") from error if b"\x00" in data: raise NoticeError(f"binary notice: {logical}") target = "inputs/" + logical + (".notice.txt" if span is not None else "") if target in payloads: raise NoticeError("output collision") payloads[target] = data records.append({**entry, "output": target, "output_size": len(data)}) payloads["README.txt"] = INTRO manifest = { "schema": 1, "snapshot": catalog["snapshot"], "catalog_sha256": digest(catalog_data), "inputs": records, "generated": [{"path": "README.txt", "sha256": digest(INTRO), "size": len(INTRO)}], } # Written last as the completion marker. The manifest does not hash itself. payloads["manifest.json"] = json_bytes(manifest) with directory(output.parent) as parent_fd: # Even an empty existing directory or dangling symlink is an error. os.mkdir(output.name, mode=0o700, dir_fd=parent_fd) out_fd = os.open(output.name, DIR_FLAGS, dir_fd=parent_fd) try: for name, data in payloads.items(): write_new(out_fd, name, data) finally: os.close(out_fd) return manifest def write_new(root_fd, path, data): fd = os.dup(root_fd) try: parts = relative_parts(path) for part in parts[:-1]: try: os.mkdir(part, mode=0o700, dir_fd=fd) except FileExistsError: pass child = os.open(part, DIR_FLAGS, dir_fd=fd) os.close(fd) fd = child target = os.open(parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=fd) with os.fdopen(target, "wb") as stream: stream.write(data) finally: os.close(fd) def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project-root", type=Path, default=Path(__file__).absolute().parent.parent) parser.add_argument("--sdk-root", type=Path, required=True) parser.add_argument("--toolchain-root", type=Path, required=True) parser.add_argument("--output", type=Path, required=True, help="new directory outside input roots; parent must already exist") args = parser.parse_args(argv) try: # No user-supplied catalog, discovery, network, install, build, or repin mode. with directory(Path(__file__).absolute().parent.parent) as policy_fd: catalog_data = read_bounded(policy_fd, CATALOG) manifest = assemble({"project": args.project_root, "sdk": args.sdk_root, "toolchain": args.toolchain_root}, args.output, catalog_data) except (NoticeError, OSError, ValueError, TypeError) as error: print(f"release notices: FAILED: {error}", file=sys.stderr) return 1 print(f"PASS: {len(manifest['inputs'])} pinned notice/provenance inputs; no release clearance") return 0 if __name__ == "__main__": sys.exit(main())