Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/.publication/lib/mechanism.py

117 lines
4.3 KiB
Python

"""Mechanism distribution and drift detection.
Gitea 1.25.5 cannot read a reusable workflow out of a private repository
(that is PR #32562, milestone 1.26.0), so the invariant scripts are copied into
consuming repositories. Copying is only safe if drift is detectable, so every
copy is pinned three ways: file hashes against a manifest, the manifest against
its own digest, and that digest against the policy record, which the consuming
repository cannot write.
"""
import hashlib
import json
import os
import shutil
PIN_FILENAME = ".publication-mechanism.json"
def canonical_json(obj):
return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def sha256_bytes(data):
return hashlib.sha256(data).hexdigest()
def compute_manifest(root, relpaths, version):
files = {}
for rel in sorted(relpaths):
path = os.path.join(root, rel)
if not os.path.isfile(path):
raise FileNotFoundError(f"mechanism: {rel} not found under {root}")
files[rel] = sha256_file(path)
return {"mechanism_version": version, "files": files}
def manifest_digest(manifest):
return sha256_bytes(canonical_json(manifest))
def write_manifest(path, manifest):
with open(path, "w", encoding="utf-8") as fh:
fh.write(json.dumps(manifest, sort_keys=True, indent=2) + "\n")
def read_manifest(path):
with open(path, "r", encoding="utf-8") as fh:
return json.load(fh)
def install(source_root, target_root, manifest, pin_dir=None):
"""Copy the manifest's files into target_root and write the pin file."""
for rel in sorted(manifest["files"]):
src = os.path.join(source_root, rel)
dst = os.path.join(target_root, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copyfile(src, dst)
os.chmod(dst, 0o755 if rel.endswith(".sh") or "/bin/" in rel else 0o644)
pin = {
"mechanism_version": manifest["mechanism_version"],
"manifest_sha256": manifest_digest(manifest),
}
pin_path = os.path.join(pin_dir or target_root, PIN_FILENAME)
with open(pin_path, "w", encoding="utf-8") as fh:
fh.write(json.dumps(pin, sort_keys=True, indent=2) + "\n")
return pin_path
def verify(target_root, manifest, policy_manifest_sha256, policy_mechanism_version):
"""Return failure reasons. Empty means the local copy is the pinned mechanism."""
reasons = []
digest = manifest_digest(manifest)
if digest != policy_manifest_sha256:
reasons.append(
"mechanism: canonical manifest digest does not match the digest pinned in "
"policy; the mechanism was changed without a policy change")
if manifest["mechanism_version"] != policy_mechanism_version:
reasons.append(
f"mechanism: manifest version {manifest['mechanism_version']} does not match "
f"policy version {policy_mechanism_version}")
pin_path = os.path.join(target_root, PIN_FILENAME)
if not os.path.isfile(pin_path):
reasons.append(f"mechanism: {PIN_FILENAME} is absent from {target_root}")
else:
try:
with open(pin_path, "r", encoding="utf-8") as fh:
pin = json.load(fh)
except json.JSONDecodeError as exc:
reasons.append(f"mechanism: {PIN_FILENAME} is not valid JSON ({exc})")
pin = None
if pin is not None:
if pin.get("manifest_sha256") != policy_manifest_sha256:
reasons.append(
"mechanism: in-repo pin disagrees with policy; a repository cannot "
"bless its own mechanism version")
if pin.get("mechanism_version") != policy_mechanism_version:
reasons.append("mechanism: in-repo pin version disagrees with policy")
for rel, expected in sorted(manifest["files"].items()):
path = os.path.join(target_root, rel)
if not os.path.isfile(path):
reasons.append(f"mechanism: {rel} is missing from the consuming repository")
continue
actual = sha256_file(path)
if actual != expected:
reasons.append(f"mechanism: {rel} has drifted from the canonical copy")
return reasons