deterministic-engine / verify_release.py
mirdax25-ux's picture
Release portable deterministic core 1.1.0: frozen formats, offline recovery and regression checks
ecad52f verified
Raw History Blame Contribute Delete
3.73 kB
#!/usr/bin/env python3
"""Verify the bounded source archive against its manifest; this is not a signature."""
import hashlib
import json
import re
import sys
from pathlib import Path, PurePosixPath
from safe_json import strict_loads
def verify_release(root):
root = Path(root).resolve()
failures = []
try:
manifest_file = root / "RELEASE_MANIFEST.json"
if manifest_file.is_symlink() or manifest_file.stat().st_size > 1_048_576:
raise ValueError("Unsafe manifest")
manifest = strict_loads(manifest_file.read_text(encoding="utf-8"))
if not isinstance(manifest, dict) or manifest.get("manifest_format") != "bodul-source-archive-v1":
raise ValueError("Unknown manifest format")
files = manifest.get("files")
if not isinstance(files, dict) or not 1 <= len(files) <= 500:
raise ValueError("Invalid file list")
folded = set()
for name, entry in files.items():
relative = PurePosixPath(name)
if not relative.parts or not re.fullmatch(r"[A-Za-z0-9_.\-/]+", name) or relative.is_absolute() or name != relative.as_posix() or any(part in (".", "..") for part in relative.parts) or name == "RELEASE_MANIFEST.json":
raise ValueError("Unsafe archive path")
if name.casefold() in folded:
raise ValueError("Case-insensitive archive path collision")
folded.add(name.casefold())
if not isinstance(entry, dict) or set(entry) != {"bytes", "sha256", "sha512"}:
raise ValueError("Invalid file entry")
if type(entry["bytes"]) is not int or not 0 <= entry["bytes"] <= 2_000_000:
raise ValueError("Invalid file size")
for algorithm, length in (("sha256", 64), ("sha512", 128)):
if not isinstance(entry[algorithm], str) or not re.fullmatch("[0-9a-f]{" + str(length) + "}", entry[algorithm]):
raise ValueError("Invalid digest")
path = root.joinpath(*relative.parts)
if not path.is_relative_to(root) or any(parent.is_symlink() for parent in (path, *path.parents) if parent != root and parent.is_relative_to(root)):
failures.append(name + ": unsafe symlink")
continue
if not path.is_file() or path.stat().st_size != entry["bytes"]:
failures.append(name + ": missing file or changed length")
continue
data = path.read_bytes()
if any(hashlib.new(a, data).hexdigest() != entry[a] for a in ("sha256", "sha512")):
failures.append(name + ": digest mismatch")
# Ignore development machinery; reject extra source/data files.
for path in root.rglob("*"):
rel = path.relative_to(root)
if any(part in (".git", ".venv", "__pycache__") for part in rel.parts):
continue
if path.is_symlink():
failures.append(rel.as_posix() + ": unexpected symlink")
elif path.is_file() and rel.as_posix() not in files and rel.as_posix() != "RELEASE_MANIFEST.json":
failures.append(rel.as_posix() + ": unlisted file")
return {"verified": not failures, "files_checked": len(files), "scope": "archive_byte_integrity", "authenticated": False, "failures": failures}
except (ValueError, TypeError, OSError, KeyError, UnicodeError) as exc:
return {"verified": False, "scope": "archive_byte_integrity", "authenticated": False, "failures": [str(exc)]}
if __name__ == "__main__":
result = verify_release(Path(__file__).resolve().parent)
print(json.dumps(result, sort_keys=True))
sys.exit(0 if result["verified"] else 2)