350 lines
12 KiB
Python
350 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
"""Authenticated Gitea consistency provider using the real governed Git transport.
|
|
|
|
The provider proves Gitea consistency without inventing a REST API:
|
|
|
|
* local HEAD == source_oid,
|
|
* local source_oid^{tree} == tree_oid,
|
|
* origin identifies the governed repository,
|
|
* exact checked-out branch exists remotely, and
|
|
* git ls-remote reports exactly source_oid for that branch.
|
|
|
|
It then writes an append-only root-owned v2 provider receipt using only governed
|
|
policy for provider identity, operation, store root, store id and modes.
|
|
|
|
The result explicitly records ``transport=git-ls-remote`` and
|
|
``api_verified=false``. It must never be described as REST/API evidence.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
from typing import Callable, Mapping
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
|
RECEIPT_SCHEMA = "aios.trusted-evidence-provider-receipt.v2"
|
|
RECEIPT_VERSION = 2
|
|
_OID_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
_REF_RE = re.compile(r"^refs/heads/[A-Za-z0-9._/-]+$")
|
|
_REPO_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
|
|
|
|
|
|
class GiteaProviderError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GitCommandResult:
|
|
returncode: int
|
|
stdout: str = ""
|
|
stderr: str = ""
|
|
|
|
|
|
GitRunner = Callable[[tuple[str, ...], Path], GitCommandResult]
|
|
|
|
|
|
def canonical_json_bytes(payload: Mapping) -> bytes:
|
|
return json.dumps(
|
|
dict(payload),
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
).encode("utf-8")
|
|
|
|
|
|
def canonical_sha256(payload: Mapping) -> str:
|
|
return hashlib.sha256(canonical_json_bytes(payload)).hexdigest()
|
|
|
|
|
|
def _runner(args: tuple[str, ...], cwd: Path) -> GitCommandResult:
|
|
proc = subprocess.run(
|
|
list(args),
|
|
cwd=str(cwd),
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
timeout=45,
|
|
check=False,
|
|
)
|
|
return GitCommandResult(proc.returncode, proc.stdout, proc.stderr)
|
|
|
|
|
|
def _git(run: GitRunner, cwd: Path, *args: str, error: str) -> str:
|
|
result = run(("git", *args), cwd)
|
|
if result.returncode != 0:
|
|
raise GiteaProviderError(f"{error}: {result.stderr.strip()[:300]}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _oid(value: str, field: str) -> str:
|
|
value = str(value).strip().lower()
|
|
if not _OID_RE.fullmatch(value):
|
|
raise GiteaProviderError(f"{field.upper()}_INVALID")
|
|
return value
|
|
|
|
|
|
def _mode(value) -> int:
|
|
if isinstance(value, int):
|
|
return value
|
|
text = str(value).strip()
|
|
if text.isdigit():
|
|
return int(text, 8)
|
|
raise GiteaProviderError("PROVIDER_MODE_INVALID")
|
|
|
|
|
|
def _safe_ref(value: str) -> Path:
|
|
raw = str(value).strip()
|
|
p = Path(raw)
|
|
if not raw or p.is_absolute() or any(part in ("", ".", "..") for part in p.parts):
|
|
raise GiteaProviderError("PROVIDER_RECEIPT_REF_INVALID")
|
|
return p
|
|
|
|
|
|
def _safe_remote_url(url: str) -> str:
|
|
url = str(url).strip()
|
|
if "://" in url:
|
|
parts = urlsplit(url)
|
|
host = parts.hostname or ""
|
|
port = f":{parts.port}" if parts.port else ""
|
|
return urlunsplit((parts.scheme, host + port, parts.path, "", ""))
|
|
if ":" in url and not url.startswith(("/", "./", "../")):
|
|
left, right = url.split(":", 1)
|
|
return f"{left.rsplit('@', 1)[-1]}:{right}"
|
|
return url
|
|
|
|
|
|
def repository_identity_from_remote(url: str) -> str:
|
|
safe = _safe_remote_url(url)
|
|
if "://" in safe:
|
|
path = urlsplit(safe).path
|
|
elif ":" in safe and not safe.startswith("/"):
|
|
path = safe.split(":", 1)[1]
|
|
else:
|
|
path = safe
|
|
parts = [x for x in path.strip("/").split("/") if x]
|
|
if parts and parts[0] == "git":
|
|
parts = parts[1:]
|
|
if len(parts) < 2:
|
|
raise GiteaProviderError("GITEA_REPOSITORY_IDENTITY_UNAVAILABLE")
|
|
owner, repo = parts[-2], parts[-1]
|
|
if repo.endswith(".git"):
|
|
repo = repo[:-4]
|
|
identity = f"{owner}/{repo}".lower()
|
|
if not _REPO_RE.fullmatch(identity):
|
|
raise GiteaProviderError("GITEA_REPOSITORY_IDENTITY_INVALID")
|
|
return identity
|
|
|
|
|
|
def load_gitea_policy(policy_path: str | Path) -> dict:
|
|
raw = json.loads(Path(policy_path).read_text(encoding="utf-8"))
|
|
defaults = dict(raw.get("defaults") or {})
|
|
providers = raw.get("providers")
|
|
if not isinstance(providers, dict) or not isinstance(providers.get("gitea"), dict):
|
|
raise GiteaProviderError("GITEA_PROVIDER_POLICY_MISSING")
|
|
cfg = dict(providers["gitea"])
|
|
return {
|
|
"provider_identity": str(cfg["provider_identity"]),
|
|
"store_id": str(cfg["store_id"]),
|
|
"store_root": Path(str(cfg["store_root"])),
|
|
"provider_operation": str(cfg["allowed_operations"][0]),
|
|
"expected_owner_uid": int(cfg.get("expected_owner_uid", defaults.get("expected_owner_uid", 0))),
|
|
"directory_mode": min(
|
|
_mode(x) for x in cfg.get(
|
|
"allowed_directory_modes",
|
|
defaults.get("allowed_directory_modes", ["0700", "0750"]),
|
|
)
|
|
),
|
|
"receipt_mode": min(
|
|
_mode(x) for x in cfg.get(
|
|
"allowed_receipt_modes",
|
|
defaults.get("allowed_receipt_modes", ["0600", "0640"]),
|
|
)
|
|
),
|
|
"authentication_kind": str(
|
|
cfg.get(
|
|
"authentication_kind",
|
|
defaults.get("authentication_kind", "root-owned-store-v1"),
|
|
)
|
|
),
|
|
}
|
|
|
|
|
|
def collect_gitea_transport_consistency(
|
|
*,
|
|
repo_root: str | Path,
|
|
source_oid: str,
|
|
tree_oid: str,
|
|
expected_repository: str = "aios/aios",
|
|
remote: str = "origin",
|
|
git_runner: GitRunner | None = None,
|
|
) -> dict:
|
|
repo_root = Path(repo_root).resolve()
|
|
if not repo_root.is_dir():
|
|
raise GiteaProviderError("GITEA_CHECKOUT_UNAVAILABLE")
|
|
source_oid = _oid(source_oid, "source_oid")
|
|
tree_oid = _oid(tree_oid, "tree_oid")
|
|
expected_repository = str(expected_repository).strip().lower()
|
|
if not _REPO_RE.fullmatch(expected_repository):
|
|
raise GiteaProviderError("GITEA_EXPECTED_REPOSITORY_INVALID")
|
|
if not re.fullmatch(r"[A-Za-z0-9._/-]+", remote) or remote.startswith(("/", "-")):
|
|
raise GiteaProviderError("GITEA_REMOTE_NAME_INVALID")
|
|
|
|
run = git_runner or _runner
|
|
|
|
if _git(run, repo_root, "rev-parse", "--is-inside-work-tree",
|
|
error="GITEA_LOCAL_REPOSITORY_REQUIRED").lower() != "true":
|
|
raise GiteaProviderError("GITEA_LOCAL_REPOSITORY_REQUIRED")
|
|
|
|
actual_source = _oid(
|
|
_git(run, repo_root, "rev-parse", "HEAD^{commit}",
|
|
error="GITEA_LOCAL_SOURCE_UNAVAILABLE"),
|
|
"actual_source_oid",
|
|
)
|
|
if actual_source != source_oid:
|
|
raise GiteaProviderError("GITEA_LOCAL_SOURCE_OID_MISMATCH")
|
|
|
|
actual_tree = _oid(
|
|
_git(run, repo_root, "rev-parse", f"{source_oid}^{{tree}}",
|
|
error="GITEA_LOCAL_TREE_UNAVAILABLE"),
|
|
"actual_tree_oid",
|
|
)
|
|
if actual_tree != tree_oid:
|
|
raise GiteaProviderError("GITEA_LOCAL_TREE_OID_MISMATCH")
|
|
|
|
branch = _git(
|
|
run, repo_root, "symbolic-ref", "--quiet", "HEAD",
|
|
error="GITEA_GOVERNED_BRANCH_REQUIRED",
|
|
)
|
|
if not _REF_RE.fullmatch(branch):
|
|
raise GiteaProviderError("GITEA_GOVERNED_BRANCH_INVALID")
|
|
|
|
remote_url = _git(
|
|
run, repo_root, "remote", "get-url", remote,
|
|
error="GITEA_REMOTE_UNAVAILABLE",
|
|
)
|
|
identity = repository_identity_from_remote(remote_url)
|
|
if identity != expected_repository:
|
|
raise GiteaProviderError("GITEA_REPOSITORY_IDENTITY_MISMATCH")
|
|
|
|
output = _git(
|
|
run, repo_root, "ls-remote", "--exit-code", remote, branch,
|
|
error="GITEA_REMOTE_REF_UNAVAILABLE",
|
|
)
|
|
rows = []
|
|
for line in output.splitlines():
|
|
parts = line.split()
|
|
if len(parts) >= 2 and parts[1] == branch:
|
|
rows.append((parts[0].lower(), parts[1]))
|
|
if len(rows) != 1:
|
|
raise GiteaProviderError("GITEA_REMOTE_REF_AMBIGUOUS")
|
|
remote_oid = _oid(rows[0][0], "remote_ref_oid")
|
|
if remote_oid != source_oid:
|
|
raise GiteaProviderError("GITEA_REMOTE_REF_OID_MISMATCH")
|
|
|
|
return {
|
|
"consistent": True,
|
|
"repository": identity,
|
|
"source_commit": source_oid,
|
|
"remote_ref_oid": remote_oid,
|
|
"tree_oid": tree_oid,
|
|
"remote_ref": branch,
|
|
"remote_url": _safe_remote_url(remote_url),
|
|
"transport": "git-ls-remote",
|
|
"api_verified": False,
|
|
}
|
|
|
|
|
|
def _ensure_root_directory(path: Path, mode: int, expected_uid: int) -> None:
|
|
if os.geteuid() != expected_uid:
|
|
raise GiteaProviderError("GITEA_PROVIDER_ROOT_REQUIRED")
|
|
path.mkdir(parents=True, exist_ok=True, mode=mode)
|
|
st = os.lstat(path)
|
|
if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):
|
|
raise GiteaProviderError("GITEA_PROVIDER_STORE_INVALID")
|
|
if st.st_uid != expected_uid:
|
|
raise GiteaProviderError("GITEA_PROVIDER_STORE_OWNER_INVALID")
|
|
if stat.S_IMODE(st.st_mode) & 0o022:
|
|
raise GiteaProviderError("GITEA_PROVIDER_STORE_MODE_INVALID")
|
|
|
|
|
|
def _write_receipt_exclusive(path: Path, payload: Mapping, mode: int) -> str:
|
|
data = canonical_json_bytes(payload)
|
|
if path.exists() or path.is_symlink():
|
|
raise GiteaProviderError("GITEA_PROVIDER_RECEIPT_EXISTS")
|
|
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
flags |= getattr(os, "O_BINARY", 0)
|
|
fd = os.open(path, flags, mode)
|
|
try:
|
|
view = memoryview(data)
|
|
while view:
|
|
written = os.write(fd, view)
|
|
if written <= 0:
|
|
raise GiteaProviderError("GITEA_PROVIDER_RECEIPT_WRITE_FAILED")
|
|
view = view[written:]
|
|
os.fsync(fd)
|
|
finally:
|
|
os.close(fd)
|
|
os.chmod(path, mode)
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def produce_gitea_consistency_receipt(
|
|
*,
|
|
repo_root: str | Path,
|
|
source_oid: str,
|
|
tree_oid: str,
|
|
policy_path: str | Path,
|
|
receipt_ref: str,
|
|
expected_repository: str = "aios/aios",
|
|
) -> tuple[str, dict]:
|
|
cfg = load_gitea_policy(policy_path)
|
|
if cfg["authentication_kind"] != "root-owned-store-v1":
|
|
raise GiteaProviderError("GITEA_PROVIDER_AUTH_KIND_UNSUPPORTED")
|
|
result = collect_gitea_transport_consistency(
|
|
repo_root=repo_root,
|
|
source_oid=source_oid,
|
|
tree_oid=tree_oid,
|
|
expected_repository=expected_repository,
|
|
)
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
run_id = f"gitea-git-transport-{source_oid[:12]}-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
|
|
rel = _safe_ref(receipt_ref)
|
|
target = cfg["store_root"].joinpath(rel)
|
|
root = cfg["store_root"].resolve()
|
|
if not str(target.resolve(strict=False)).startswith(str(root) + os.sep):
|
|
raise GiteaProviderError("GITEA_PROVIDER_RECEIPT_REF_ESCAPE")
|
|
_ensure_root_directory(root, cfg["directory_mode"], cfg["expected_owner_uid"])
|
|
parent = root
|
|
for part in rel.parts[:-1]:
|
|
parent = parent / part
|
|
_ensure_root_directory(parent, cfg["directory_mode"], cfg["expected_owner_uid"])
|
|
|
|
payload = {
|
|
"schema": RECEIPT_SCHEMA,
|
|
"version": RECEIPT_VERSION,
|
|
"provider_identity": cfg["provider_identity"],
|
|
"provider_operation": cfg["provider_operation"],
|
|
"operation_run_id": run_id,
|
|
"result_reference": f"git:{result['remote_url']}#{result['remote_ref']}",
|
|
"content_sha256": canonical_sha256(result),
|
|
"source_oid": _oid(source_oid, "source_oid"),
|
|
"tree_oid": _oid(tree_oid, "tree_oid"),
|
|
"environment": "hw-isolated-linux-git-transport",
|
|
"issued_at": now,
|
|
"result": result,
|
|
"authentication": {
|
|
"kind": "root-owned-store-v1",
|
|
"store_id": cfg["store_id"],
|
|
},
|
|
}
|
|
sha = _write_receipt_exclusive(target, payload, cfg["receipt_mode"])
|
|
return sha, payload
|