fix: bind recipe identity to certified artifact bytes (DGR-025)

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.
This commit is contained in:
Dobromir Popov
2026-07-21 13:22:02 +03:00
parent 902ecde363
commit 03e97ca31a
10 changed files with 1277 additions and 69 deletions

View File

@@ -8,13 +8,16 @@ still hash to the same recipe. The DGR-027 lock manifest
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 three load-bearing parts, and each is separately fatal
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, and a digest over the ordered patch-stack bytes.
A different upstream pin, a reordered stack, or a single changed patch byte
each produce a different axis value, which produces a different recipe digest,
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.
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
@@ -36,6 +39,7 @@ 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
@@ -58,14 +62,42 @@ def _canonical_sha256(value: object) -> str:
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: a name, an upstream commit, and an ordered patch stack."""
"""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:
@@ -89,10 +121,16 @@ class RuntimePin:
@property
def runtime_version(self) -> str:
"""The exact ``runtime_version`` recipe axis value for this pin."""
"""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}"
)
@@ -166,6 +204,15 @@ def load_runtime_pin(lock_dir: Path = DEFAULT_LOCK_DIR) -> RuntimePin:
"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:
@@ -216,6 +263,8 @@ def load_runtime_pin(lock_dir: Path = DEFAULT_LOCK_DIR) -> RuntimePin:
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,
)