Files
ESP32_Serial_Swiss_Army_Knife/tools/release_notices.py
T
Commander1024 51f835c22f Harden SSH parsing and add notice tooling
- Enforce exact service and channel names with bounded failure parsing
- Add hash-pinned offline notice assembly and regression coverage
- Record advisory dispositions, provenance, integration evidence, and
  remaining gates
2026-09-16 15:06:38 +02:00

252 lines
10 KiB
Python

#!/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. Icon provenance,
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())