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

@@ -43,6 +43,7 @@ RECIPE_IDENTITY_SCHEMA_VERSION = 1
ARTIFACT_DIGEST_DOMAIN = "meshnet.model-artifact.v1"
RECIPE_DIGEST_DOMAIN = "meshnet.runtime-recipe.v1"
SHARD_BINDING_DIGEST_DOMAIN = "meshnet.shard-binding.v1"
TOKENIZER_DIGEST_DOMAIN = "meshnet.tokenizer-identity.v1"
# The axes a recipe digest commits to. Order is irrelevant (the canonical JSON
# sorts keys); membership is not — an axis missing here is an axis the tracker
@@ -71,11 +72,18 @@ MIN_CERTIFYING_NODES = 2
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
_LLAMA_CPP_RUNTIME_PIN = re.compile(
r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}$"
r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}"
r"\+build\.[0-9a-f]{64}\+artifact\.[0-9a-f]{64}$"
)
_LLAMA_CPP_BACKEND_IDS = frozenset({"llama.cpp", "llama-cpp"})
_MOVING_REFS = frozenset({"main", "master", "head", "latest", "dev", "trunk"})
# The only admissible tokenizer identity: a digest over the tokenizer's bytes.
# A denylist of moving refs cannot enumerate every mutable label (`origin/main`,
# `stable`, `release`, a re-taggable tag…), so the tracker accepts nothing that
# *could* be a label, independently of the node's identical rule.
_TOKENIZER_IDENTITY = re.compile(r"^tokenizer\.v1:[0-9a-f]{64}$")
class RecipeIdentityError(ValueError):
"""A presented identity block is malformed or internally inconsistent."""
@@ -137,8 +145,44 @@ def _runtime_pin(value: Any, backend_id: Any) -> str:
backend = _text(backend_id, "recipe.backend_id").strip().lower()
if backend in _LLAMA_CPP_BACKEND_IDS and not _LLAMA_CPP_RUNTIME_PIN.fullmatch(text):
raise RecipeIdentityError(
"'recipe.runtime_version' for llama.cpp must bind a 40-hex commit "
"and a 64-hex ordered patch-stack digest"
"'recipe.runtime_version' for llama.cpp must bind a 40-hex commit, "
"a 64-hex ordered patch-stack digest, a 64-hex build-recipe digest, "
"and the executing native artifact's 64-hex byte digest"
)
return text
def tokenizer_identity(files: Mapping[str, bytes]) -> str:
"""Independent tracker derivation of a content-addressed tokenizer identity.
Deliberately re-implemented (no `meshnet_node` import); the committed
conformance vectors pin the two derivations to each other.
"""
if not isinstance(files, Mapping) or not files:
raise RecipeIdentityError(
"tokenizer identity requires at least one named tokenizer/config byte set"
)
digests: dict[str, str] = {}
for name, body in files.items():
if not isinstance(name, str) or not name.strip():
raise RecipeIdentityError(
"tokenizer identity file names must be non-empty strings"
)
if not isinstance(body, (bytes, bytearray)):
raise RecipeIdentityError(
f"tokenizer identity for {name!r} requires the file's bytes"
)
digests[name] = hashlib.sha256(bytes(body)).hexdigest()
return "tokenizer.v1:" + _digest(TOKENIZER_DIGEST_DOMAIN, {"files": digests})
def _tokenizer_identity_value(value: Any, what: str) -> str:
text = _text(value, what)
if not _TOKENIZER_IDENTITY.fullmatch(text):
raise RecipeIdentityError(
f"{what!r} must be a content-addressed 'tokenizer.v1:<64-hex digest>' "
"identity; a label, tag, branch, or symbolic ref is a mutable pointer, "
"not the tokenizer bytes it currently resolves to"
)
return text
@@ -347,7 +391,7 @@ def parse_identity(data: Any) -> PresentedIdentity:
axes[axis] = _integer(value, f"recipe.{axis}", 1)
else:
axes[axis] = _text(value, f"recipe.{axis}")
_pin(axes["tokenizer_revision"], "recipe.tokenizer_revision")
_tokenizer_identity_value(axes["tokenizer_revision"], "recipe.tokenizer_revision")
_runtime_pin(axes["runtime_version"], axes["backend_id"])
identity = PresentedIdentity(