Append +artifact.<sha256> to the llama.cpp runtime axis, computed from the exact bytes read by attest_loaded_runtime, so a differently-built shared object with copied lock values can no longer forge a certified runtime identity. Node/tracker parsers require the suffix; new test proves a byte-identical-lock but different-binary artifact produces a different recipe fingerprint. Regenerates conformance vectors accordingly. 105 passed in tests/test_native_identity_emission.py, tests/test_runtime_pin_identity.py, tests/test_runtime_recipe_identity.py.
271 lines
11 KiB
Python
271 lines
11 KiB
Python
"""Canonical runtime pin identity for the recipe fingerprint (DGR-025).
|
|
|
|
The recipe digest (:mod:`meshnet_node.runtime_recipe`) commits to a
|
|
``runtime_version`` axis, but a string the operator typed is a label, not a
|
|
pin: two workers could run different patch stacks under the same label and
|
|
still hash to the same recipe. The DGR-027 lock manifest
|
|
(``packages/node/native/llama``) already records the one exact upstream commit
|
|
and the ordered patch stack the native runtime is built from, so the axis
|
|
value is *derived* from that manifest, never asserted.
|
|
|
|
The derived value has four load-bearing parts, and each is separately fatal
|
|
to compatibility: the runtime name (from the upstream URL), the exact
|
|
40-character upstream commit, a digest over the ordered patch-stack bytes, and
|
|
a digest over the numerically relevant build recipe. A different upstream pin,
|
|
a reordered stack, a single changed patch byte, or a changed build flag each
|
|
produce a different axis value, which produces a different recipe digest,
|
|
which partitions the route — exactly the fail-closed behavior DGR-025 asks
|
|
for. The pin also records the patched source tree's git tree id, which the
|
|
executing runtime's attestation is compared against
|
|
(:mod:`meshnet_node.native_backend`).
|
|
|
|
Every consistency check here fails closed. The manifest keeps three records of
|
|
the stack — ``UPSTREAM_LOCK.json``'s ``patch_series``, ``patches/series``, and
|
|
``patches/SHA256SUMS`` — plus the ``UPSTREAM_COMMIT`` convenience file, and a
|
|
disagreement between any two of them means the workspace's identity is
|
|
unknowable, not "probably fine". This module reads the committed manifest
|
|
only; fetching and patching the actual source tree stays with
|
|
``scripts/llama_cpp_dependency.py`` (DGR-027).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
# Domain separation, matching the runtime_recipe digest convention: a patch
|
|
# stack digest can never be confused with an artifact or recipe digest.
|
|
PATCH_STACK_DIGEST_DOMAIN = "meshnet.runtime-patch-stack.v1"
|
|
BUILD_RECIPE_DIGEST_DOMAIN = "meshnet.runtime-build-recipe.v1"
|
|
|
|
# The UPSTREAM_LOCK.json layout this reader understands (DGR-027 schema).
|
|
RUNTIME_PIN_SCHEMA_VERSION = 1
|
|
|
|
# The committed DGR-027 manifest for the llama.cpp runtime.
|
|
DEFAULT_LOCK_DIR = Path(__file__).resolve().parent.parent / "native" / "llama"
|
|
|
|
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
|
|
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
|
|
class RuntimePinError(ValueError):
|
|
"""The lock workspace is missing, malformed, or internally inconsistent."""
|
|
|
|
|
|
def _canonical_sha256(value: object) -> str:
|
|
payload = json.dumps(
|
|
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
)
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def build_recipe_digest(build: object) -> str:
|
|
"""A digest over the numerically relevant build recipe.
|
|
|
|
The lock's ``build`` object records what the runtime is compiled *as* —
|
|
the configure flags, standards, and targets that select kernels and
|
|
numeric behavior. Two binaries built from one patched tree with different
|
|
build recipes can disagree numerically, so the recipe is part of runtime
|
|
identity. This is deliberately a digest over the *recorded recipe*, not a
|
|
compiler-specific binary SHA: reproducible-binary attestation is not
|
|
claimed here.
|
|
"""
|
|
if not isinstance(build, dict) or not build:
|
|
raise RuntimePinError(
|
|
"the lock's 'build' section must be a non-empty JSON object; a "
|
|
"runtime with an unstated build recipe has an unknowable identity"
|
|
)
|
|
return _canonical_sha256(
|
|
{"domain": BUILD_RECIPE_DIGEST_DOMAIN, "body": build}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimePin:
|
|
"""One exact runtime: name, upstream commit, patched tree, patch stack, build.
|
|
|
|
``patched_tree`` is the git tree object id of the source tree *after* the
|
|
ordered patch stack is applied — what the runtime was actually compiled
|
|
from, as distinct from the upstream commit it started from.
|
|
"""
|
|
|
|
runtime_name: str
|
|
upstream_commit: str
|
|
patched_tree: str
|
|
patch_series: tuple[str, ...]
|
|
patch_digests: tuple[str, ...]
|
|
build_recipe_digest: str
|
|
|
|
@property
|
|
def patch_stack_digest(self) -> str:
|
|
"""A digest over the ordered (name, bytes-digest) stack.
|
|
|
|
Order is digested deliberately: applying the same patches in a
|
|
different order can produce a different tree, so a reordered stack is
|
|
a different runtime.
|
|
"""
|
|
return _canonical_sha256(
|
|
{
|
|
"domain": PATCH_STACK_DIGEST_DOMAIN,
|
|
"body": {
|
|
"patches": [
|
|
[name, digest]
|
|
for name, digest in zip(self.patch_series, self.patch_digests)
|
|
]
|
|
},
|
|
}
|
|
)
|
|
|
|
@property
|
|
def runtime_version(self) -> str:
|
|
"""The exact ``runtime_version`` recipe axis value for this pin.
|
|
|
|
Commits to the runtime name, the exact upstream commit, the ordered
|
|
patch stack, and the numerically relevant build recipe — each
|
|
separately fatal to compatibility.
|
|
"""
|
|
return (
|
|
f"{self.runtime_name}@{self.upstream_commit}"
|
|
f"+patchstack.{self.patch_stack_digest}"
|
|
f"+build.{self.build_recipe_digest}"
|
|
)
|
|
|
|
|
|
def _read_text(path: Path, what: str) -> str:
|
|
try:
|
|
return path.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
raise RuntimePinError(f"{what} not found at {path}") from None
|
|
except OSError as exc:
|
|
raise RuntimePinError(f"{what} at {path} is unreadable: {exc}") from exc
|
|
|
|
|
|
def _read_series_file(path: Path) -> list[str]:
|
|
lines = _read_text(path, "patches/series").splitlines()
|
|
return [line.strip() for line in lines if line.strip() and not line.startswith("#")]
|
|
|
|
|
|
def _read_sums_file(path: Path) -> list[tuple[str, str]]:
|
|
entries: list[tuple[str, str]] = []
|
|
for line in _read_text(path, "patches/SHA256SUMS").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.split(None, 1)
|
|
if len(parts) != 2 or not _HEX64.match(parts[0]):
|
|
raise RuntimePinError(
|
|
"patches/SHA256SUMS contains a line that is not "
|
|
"'<sha256> <patch name>'"
|
|
)
|
|
entries.append((parts[0], parts[1].strip()))
|
|
return entries
|
|
|
|
|
|
def load_runtime_pin(lock_dir: Path = DEFAULT_LOCK_DIR) -> RuntimePin:
|
|
"""Derive the exact runtime pin from a DGR-027 lock workspace, or refuse.
|
|
|
|
Refuses — rather than guessing — on a missing or malformed lock, a moving
|
|
commit reference, a disagreement between the lock's ``patch_series``, the
|
|
``patches/series`` file, ``patches/SHA256SUMS``, or the actual patch
|
|
bytes, and on an ``UPSTREAM_COMMIT`` file that names a different commit.
|
|
"""
|
|
lock_path = lock_dir / "UPSTREAM_LOCK.json"
|
|
raw = _read_text(lock_path, "UPSTREAM_LOCK.json")
|
|
try:
|
|
lock = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise RuntimePinError(f"UPSTREAM_LOCK.json is not valid JSON: {exc}") from exc
|
|
if not isinstance(lock, dict):
|
|
raise RuntimePinError("UPSTREAM_LOCK.json must be a JSON object")
|
|
|
|
schema = lock.get("schema_version")
|
|
if schema != RUNTIME_PIN_SCHEMA_VERSION:
|
|
raise RuntimePinError(
|
|
f"UPSTREAM_LOCK.json declares schema version {schema!r}; this reader "
|
|
f"understands version {RUNTIME_PIN_SCHEMA_VERSION}"
|
|
)
|
|
|
|
upstream = lock.get("upstream")
|
|
if not isinstance(upstream, str) or not upstream.strip():
|
|
raise RuntimePinError("UPSTREAM_LOCK.json is missing the upstream URL")
|
|
runtime_name = upstream.rstrip("/").rsplit("/", 1)[-1]
|
|
if runtime_name.endswith(".git"):
|
|
runtime_name = runtime_name[: -len(".git")]
|
|
if not runtime_name:
|
|
raise RuntimePinError("the upstream URL does not name a runtime")
|
|
|
|
commit = lock.get("commit")
|
|
if not isinstance(commit, str) or not _HEX40.match(commit):
|
|
raise RuntimePinError(
|
|
f"UPSTREAM_LOCK.json commit {commit!r} is not an exact 40-character "
|
|
"hexadecimal object id; a moving reference is not a pin"
|
|
)
|
|
|
|
patched_tree = lock.get("patched_tree")
|
|
if not isinstance(patched_tree, str) or not _HEX40.match(patched_tree):
|
|
raise RuntimePinError(
|
|
"UPSTREAM_LOCK.json patched_tree must be the exact 40-character git "
|
|
"tree object id of the source tree after the patch stack is applied"
|
|
)
|
|
|
|
build_digest = build_recipe_digest(lock.get("build"))
|
|
|
|
commit_file = _read_text(lock_dir / "UPSTREAM_COMMIT", "UPSTREAM_COMMIT")
|
|
recorded = commit_file.strip().splitlines()[0].strip() if commit_file.strip() else ""
|
|
if recorded != commit:
|
|
raise RuntimePinError(
|
|
"UPSTREAM_COMMIT and UPSTREAM_LOCK.json disagree on the pinned commit"
|
|
)
|
|
|
|
lock_series = lock.get("patch_series")
|
|
if not isinstance(lock_series, list) or not all(
|
|
isinstance(name, str) and name.strip() for name in lock_series
|
|
):
|
|
raise RuntimePinError(
|
|
"UPSTREAM_LOCK.json patch_series must be a list of patch file names"
|
|
)
|
|
if len(set(lock_series)) != len(lock_series):
|
|
raise RuntimePinError("UPSTREAM_LOCK.json patch_series contains a duplicate")
|
|
|
|
series = _read_series_file(lock_dir / "patches" / "series")
|
|
if series != lock_series:
|
|
raise RuntimePinError(
|
|
"patches/series and UPSTREAM_LOCK.json patch_series disagree on the "
|
|
"ordered patch stack"
|
|
)
|
|
|
|
sums = _read_sums_file(lock_dir / "patches" / "SHA256SUMS")
|
|
if [name for _, name in sums] != lock_series:
|
|
raise RuntimePinError(
|
|
"patches/SHA256SUMS does not record exactly the ordered patch stack "
|
|
"named by UPSTREAM_LOCK.json"
|
|
)
|
|
|
|
digests: list[str] = []
|
|
for (expected_digest, name) in sums:
|
|
patch_path = lock_dir / "patches" / name
|
|
try:
|
|
body = patch_path.read_bytes()
|
|
except FileNotFoundError:
|
|
raise RuntimePinError(f"patch file {name} is named but missing") from None
|
|
except OSError as exc:
|
|
raise RuntimePinError(f"patch file {name} is unreadable: {exc}") from exc
|
|
actual = hashlib.sha256(body).hexdigest()
|
|
if actual != expected_digest:
|
|
raise RuntimePinError(
|
|
f"patch file {name} does not match its patches/SHA256SUMS digest"
|
|
)
|
|
digests.append(actual)
|
|
|
|
return RuntimePin(
|
|
runtime_name=runtime_name,
|
|
upstream_commit=commit,
|
|
patched_tree=patched_tree,
|
|
patch_series=tuple(lock_series),
|
|
patch_digests=tuple(digests),
|
|
build_recipe_digest=build_digest,
|
|
)
|