reuse-surface/reuse_surface/private_source_verify.py
tegwick b181240b35 Protect authenticated federation sources with exact runtime URL bindings
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a137ca-5c77-7a71-b9ae-23ad9b0b5684
2026-10-15 01:25:49 +02:00

178 lines
7.5 KiB
Python

"""Owner-prepared, receipt-only native acquisition check for CUST81 revision 1.
Run inside the hub environment after a separately approved credential rollout:
python -m reuse_surface.private_source_verify --proposal FILE --disclosure FILE
--consent FILE
No credentials are accepted as arguments. No bodies or exception text are emitted.
This command does not register sources or use the federation cache.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import quote
import yaml
from jsonschema import Draft202012Validator
from reuse_surface import federation
from reuse_surface.registry import load_schema
PROPOSAL_SHA256 = "7819eb51d249f5c7aff4df9fb520afcfb76603b05a4104439499102b6c20af99"
DISCLOSURE_SHA256 = "46ac4fe49a08054cc215aade2fbaf3b36ee334520997857ed84f530b52da9d35"
CONSENT_SHA256 = "4a4602ad926e3af855d6ddba4731430d33e4a7432c507ba0a5193b9f43056b52"
AUTH_ENV = "REUSE_SURFACE_FORGEJO_SOURCE_READ_TOKEN"
REPOSITORIES = frozenset({
"rapp-openbao", "rapp-policy-nexus", "rapp-postgres", "rapp-sbom-nexus",
"rapp-telemetry", "rapp-tenant-engine", "rapp-user-engine",
})
def _sha(content: bytes) -> str:
return hashlib.sha256(content).hexdigest()
def _load(path: Path, expected: str) -> dict[str, Any]:
data = path.read_bytes()
if _sha(data) != expected:
raise ValueError("packet_digest_mismatch")
return json.loads(data)
def _raw_url(repo: str, commit: str, path: str) -> str:
if not re.fullmatch(r"[0-9a-f]{40}", commit):
raise ValueError("invalid_commit")
if not re.fullmatch(r"registry/(indexes|capabilities)/[a-zA-Z0-9._-]+\.(yaml|md)", path):
raise ValueError("invalid_metadata_path")
return (
f"https://forgejo.coulomb.social/api/v1/repos/coulomb/{repo}/raw/"
f"{quote(path, safe='/')}?ref={commit}"
)
def _prepared_sources(proposal_path: Path, disclosure_path: Path, consent_path: Path):
proposal = _load(proposal_path, PROPOSAL_SHA256)
disclosure = _load(disclosure_path, DISCLOSURE_SHA256)
consent = _load(consent_path, CONSENT_SHA256)
if (
consent["consent_status"] != "approved"
or consent["proposal_sha256"] != DISCLOSURE_SHA256
or consent["proposal_id"] != disclosure["id"]
or consent["proposal_revision"] != disclosure["revision"]
or set(consent["repositories"]) != REPOSITORIES
):
raise ValueError("invalid_consent")
sources = proposal["sources"]
approved = {source["repo"]: source for source in disclosure["sources"]}
if len(sources) != 7 or {source["repo"] for source in sources} != REPOSITORIES:
raise ValueError("invalid_source_scope")
for source in sources:
owner = approved[source["repo"]]
files = {item["path"]: item["sha256"] for item in owner["files"]}
if (
source["consented_commit"] != owner["commit"]
or source["capability_ids"] != owner["capability_ids"]
or source["index_sha256"] != files["registry/indexes/capabilities.yaml"]
or source["auth_env"] != AUTH_ENV
or source["native_raw_url"] != _raw_url(
source["repo"], owner["commit"], "registry/indexes/capabilities.yaml"
)
):
raise ValueError("source_not_consented")
yield source, files
def verify(proposal_path: Path, disclosure_path: Path, consent_path: Path) -> dict[str, Any]:
receipt: dict[str, Any] = {
"id": "CUST81-PRIVATE-NATIVE-FETCH-RECEIPT-R1",
"checked_at": datetime.now(timezone.utc).isoformat(),
"proposal_sha256": PROPOSAL_SHA256,
"disclosure_sha256": DISCLOSURE_SHA256,
"consent_sha256": CONSENT_SHA256,
"cache_used": False,
"all_verified": False,
"sources": [],
}
try:
# Exhaust all packet checks before any native network request.
sources = list(_prepared_sources(proposal_path, disclosure_path, consent_path))
expected_urls = {
_raw_url(source["repo"], source["consented_commit"], path)
for source, files in sources for path in files
}
allowlist = json.loads(os.environ.get("REUSE_SURFACE_AUTH_SOURCE_ALLOWLIST", "{}"))
actual_urls = allowlist.get(AUTH_ENV)
if (
not isinstance(actual_urls, list)
or len(actual_urls) != 14
or set(actual_urls) != expected_urls
):
raise ValueError("runtime_scope_not_exact")
schema = Draft202012Validator(load_schema())
except Exception:
receipt["error"] = "packet_scope_or_schema_validation_failed"
return receipt
for source, files in sources:
row = {
"repo": source["repo"],
"commit": source["consented_commit"],
"index_sha256": source["index_sha256"],
"verified": False,
"entry_sha256": [],
}
receipt["sources"].append(row)
try:
index_text = federation.fetch_remote_index_text(source["native_raw_url"], source)
if _sha(index_text.encode("utf-8")) != source["index_sha256"]:
raise ValueError("index_hash_mismatch")
index = yaml.safe_load(index_text)
entries = index["capabilities"]
if not isinstance(entries, list) or [e["id"] for e in entries] != source["capability_ids"]:
raise ValueError("unexpected_capability_ids")
for entry in entries:
if not {"id", "name", "summary", "vector", "domain", "status", "owner", "path"} <= entry.keys():
raise ValueError("invalid_index_row")
if entry["owner"] != source["repo"] or entry["path"] not in files:
raise ValueError("unconsented_entry")
entry_url = _raw_url(source["repo"], source["consented_commit"], entry["path"])
entry_text = federation.fetch_remote_index_text(entry_url, source)
digest = _sha(entry_text.encode("utf-8"))
if digest != files[entry["path"]]:
raise ValueError("entry_hash_mismatch")
front = re.match(r"^---\n(.*?)\n---", entry_text, re.DOTALL)
if front is None:
raise ValueError("missing_front_matter")
metadata = yaml.safe_load(front.group(1))
schema.validate(metadata)
if metadata["id"] != entry["id"] or metadata["owner"] != source["repo"]:
raise ValueError("entry_identity_mismatch")
row["entry_sha256"].append(digest)
row["verified"] = True
except Exception:
# Never stringify errors: HTTP handlers and parsers can embed bodies,
# headers, credentials, or private source text in their messages.
row["error"] = "native_fetch_or_metadata_validation_failed"
receipt["all_verified"] = len(receipt["sources"]) == 7 and all(
row["verified"] for row in receipt["sources"]
)
return receipt
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("proposal", "disclosure", "consent"):
parser.add_argument(f"--{name}", type=Path, required=True)
args = parser.parse_args()
receipt = verify(args.proposal, args.disclosure, args.consent)
print(json.dumps(receipt, sort_keys=True))
return 0 if receipt["all_verified"] else 1
if __name__ == "__main__":
raise SystemExit(main())