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