diff --git a/.github/workflows/integrity.yml b/.github/workflows/integrity.yml new file mode 100644 index 0000000..be90e64 --- /dev/null +++ b/.github/workflows/integrity.yml @@ -0,0 +1,19 @@ +name: Integrity + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + checksum-domain: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Verify declared checksum byte domains + run: python3 tools/verify_checksum_domain.py diff --git a/CHECKSUM_DOMAIN.json b/CHECKSUM_DOMAIN.json new file mode 100644 index 0000000..5987f6e --- /dev/null +++ b/CHECKSUM_DOMAIN.json @@ -0,0 +1,49 @@ +{ + "schema_version": "checksum-domain.v1", + "document_id": "world-intelligence-v1.1.0-checksum-domain", + "last_verified": "2026-08-07", + "repository": "https://github.com/Kot141078/world-intelligence", + "observed_head_commit": "069c995703ef1648f06e1363fb37491835ed363e", + "scope": "Interpretation layer for repository-layout manifests; release-asset and metadata-only manifests remain raw-byte domains.", + "authority_rule": "The two repository-layout manifests recorded CRLF representations of .gitattributes and .gitignore. Current Git blobs and checkout policy use LF. This declaration makes that byte-domain conversion explicit without altering publication or release bytes.", + "claim_boundary": "A checksum match establishes byte identity in the declared hash domain only; it does not establish authorship beyond cited metadata, completeness, translation quality, scientific validity, safety, or deployment status.", + "manifests": [ + { + "path": "hashes/SHA256SUMS.repo-all.txt", + "hash_algorithm": "sha256", + "default_mode": "raw_bytes", + "path_modes": { + "crlf_text_bytes": [ + ".gitattributes", + ".gitignore" + ] + }, + "expected_entries": 43 + }, + { + "path": "hashes/SHA256SUMS.repo-layout.txt", + "hash_algorithm": "sha256", + "default_mode": "raw_bytes", + "path_modes": { + "crlf_text_bytes": [ + ".gitattributes", + ".gitignore" + ] + }, + "expected_entries": 39 + }, + { + "path": "hashes/SHA256SUMS.metadata-only.txt", + "hash_algorithm": "sha256", + "default_mode": "raw_bytes", + "path_modes": { + "crlf_text_bytes": [] + }, + "expected_entries": 10 + } + ], + "verification": { + "command": "python3 tools/verify_checksum_domain.py", + "normalization_rule": "For crlf_text_bytes only, an LF checkout is transformed to CRLF before hashing. Existing CRLF bytes are hashed unchanged; mixed or bare-CR input is rejected." + } +} diff --git a/tools/verify_checksum_domain.py b/tools/verify_checksum_domain.py new file mode 100644 index 0000000..16cbec9 --- /dev/null +++ b/tools/verify_checksum_domain.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import hashlib +import json +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +CONFIG_PATH = ROOT / "CHECKSUM_DOMAIN.json" +SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$") + + +def fail(message: str) -> None: + raise ValueError(message) + + +def safe_file(relative: str) -> Path: + path = Path(relative) + if path.is_absolute() or ".." in path.parts: + fail(f"Unsafe manifest path: {relative!r}") + resolved = (ROOT / path).resolve() + if ROOT.resolve() not in resolved.parents: + fail(f"Manifest path escapes repository: {relative!r}") + return resolved + + +def crlf_bytes(data: bytes, relative: str) -> bytes: + if b"\r\n" in data: + if data.replace(b"\r\n", b"").find(b"\r") >= 0: + fail(f"Mixed or bare-CR text is not admissible: {relative}") + return data + if b"\r" in data: + fail(f"Bare-CR text is not admissible: {relative}") + return data.replace(b"\n", b"\r\n") + + +def manifest_entries(path: Path) -> list[tuple[str, str]]: + entries: list[tuple[str, str]] = [] + seen: set[str] = set() + for line_number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), start=1): + if not line.strip() or line.lstrip().startswith("#"): + continue + parts = line.split(None, 1) + if len(parts) != 2 or not SHA256_RE.fullmatch(parts[0]): + fail(f"Malformed checksum line {path.relative_to(ROOT)}:{line_number}") + relative = parts[1].lstrip("*").strip() + if relative in seen: + fail(f"Duplicate checksum path in {path.relative_to(ROOT)}: {relative}") + seen.add(relative) + entries.append((parts[0].lower(), relative)) + return entries + + +def main() -> int: + try: + config = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + if config.get("schema_version") != "checksum-domain.v1": + fail("Unsupported checksum-domain schema_version") + checked = 0 + for declaration in config["manifests"]: + manifest_path = safe_file(declaration["path"]) + entries = manifest_entries(manifest_path) + if len(entries) != declaration["expected_entries"]: + fail(f"Entry count mismatch for {declaration['path']}") + crlf_paths = set(declaration["path_modes"].get("crlf_text_bytes", [])) + entry_paths = {relative for _, relative in entries} + if not crlf_paths <= entry_paths: + fail(f"Declared CRLF path is absent from {declaration['path']}") + for expected, relative in entries: + data = safe_file(relative).read_bytes() + if relative in crlf_paths: + data = crlf_bytes(data, relative) + actual = hashlib.sha256(data).hexdigest() + if actual != expected: + fail(f"Checksum mismatch in {declaration['path']}: {relative}") + checked += 1 + print(f"PASS checksum-domain.v1: {checked} manifest entries") + return 0 + except (OSError, KeyError, TypeError, json.JSONDecodeError, ValueError) as exc: + print(f"FAIL {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())