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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user