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:
@@ -73,6 +73,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 of a runtime recipe. Every one of these changes the numbers a Shard
|
||||
# produces, so every one of them is part of identity and none of them may be
|
||||
@@ -126,12 +127,22 @@ _AXIS_MISMATCH: Mapping[str, str] = {
|
||||
|
||||
_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"})
|
||||
|
||||
# A revision that can move is not a pin. DGR-017 learned this on the artifact;
|
||||
# it is just as true of a tokenizer.
|
||||
# The one shape a tokenizer identity may take: a digest over the tokenizer's
|
||||
# actual bytes (see `tokenizer_identity`). Any *label* — `origin/main`,
|
||||
# `stable`, `release`, a tag, a symbolic ref — names a mutable pointer, and a
|
||||
# denylist of known-mutable names can never enumerate them all. So the check is
|
||||
# inverted: instead of rejecting labels we recognize as moving, accept only a
|
||||
# value that could not be a label in the first place.
|
||||
_TOKENIZER_IDENTITY = re.compile(r"^tokenizer\.v1:[0-9a-f]{64}$")
|
||||
|
||||
# A revision that can move is not a pin. DGR-017 learned this on the artifact.
|
||||
# Used for diagnosis-only fields (`artifact.revision`); the digested tokenizer
|
||||
# axis requires the strictly stronger `_TOKENIZER_IDENTITY` form.
|
||||
_MOVING_REFS = frozenset({"main", "master", "head", "latest", "dev", "trunk"})
|
||||
|
||||
|
||||
@@ -204,7 +215,50 @@ def _require_runtime_pin(value: Any, backend_id: Any) -> str:
|
||||
if backend in _LLAMA_CPP_BACKEND_IDS and not _LLAMA_CPP_RUNTIME_PIN.fullmatch(text):
|
||||
raise RecipeIdentityError(
|
||||
"'recipe.runtime_version' for llama.cpp must be "
|
||||
"'llama.cpp@<40-hex commit>+patchstack.<64-hex digest>'"
|
||||
"'llama.cpp@<40-hex commit>+patchstack.<64-hex digest>"
|
||||
"+build.<64-hex digest>+artifact.<64-hex digest>'"
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
def tokenizer_identity(files: Mapping[str, bytes]) -> str:
|
||||
"""The content-addressed identity of a tokenizer: a digest over its bytes.
|
||||
|
||||
`files` maps each numerically relevant tokenizer/config file name — for a
|
||||
GGUF, the embedded tokenizer metadata blob; for a safetensors deployment,
|
||||
`tokenizer.json`, `tokenizer_config.json`, `special_tokens_map.json` — to
|
||||
that file's exact bytes. The identity commits to each name and each byte
|
||||
set, so two tokenizers published under one label differ, and a one-byte
|
||||
edit is a different tokenizer.
|
||||
"""
|
||||
if not isinstance(files, Mapping) or not files:
|
||||
raise RecipeIdentityError(
|
||||
"tokenizer identity requires at least one named tokenizer/config "
|
||||
"byte set; an identity over nothing pins nothing"
|
||||
)
|
||||
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, "
|
||||
"not a path or label"
|
||||
)
|
||||
digests[name] = hashlib.sha256(bytes(body)).hexdigest()
|
||||
return "tokenizer.v1:" + _digest(TOKENIZER_DIGEST_DOMAIN, {"files": digests})
|
||||
|
||||
|
||||
def _require_tokenizer_identity(value: Any, what: str) -> str:
|
||||
text = _require_text(value, what)
|
||||
if not _TOKENIZER_IDENTITY.fullmatch(text):
|
||||
raise RecipeIdentityError(
|
||||
f"{what!r} must be a content-addressed tokenizer identity "
|
||||
"'tokenizer.v1:<64-hex digest>' derived from the tokenizer's bytes "
|
||||
"(tokenizer_identity); a repository label, tag, branch, or symbolic "
|
||||
"ref names a mutable pointer, not the bytes it currently resolves to"
|
||||
)
|
||||
return text
|
||||
|
||||
@@ -389,10 +443,13 @@ class RuntimeRecipe:
|
||||
one in fp16, produce different logits from the same bytes. Keeping the axes
|
||||
apart is the entire safety property; see :data:`RECIPE_AXES`.
|
||||
|
||||
`tokenizer_revision` and `runtime_version` must be exact pins, never moving
|
||||
references. For the native runtime the canonical `runtime_version` value —
|
||||
committing to the exact upstream commit *and* the ordered patch stack — is
|
||||
derived from the DGR-027 lock manifest by :mod:`meshnet_node.runtime_pin`.
|
||||
`tokenizer_revision` must be a content-addressed tokenizer identity
|
||||
(:func:`tokenizer_identity`) — a digest over the tokenizer's actual bytes,
|
||||
never a repository label that merely points at bytes. `runtime_version`
|
||||
must be an exact pin; for the native runtime the canonical value —
|
||||
committing to the exact upstream commit, the ordered patch stack, *and*
|
||||
the numerically relevant build recipe — is derived from the DGR-027 lock
|
||||
manifest by :mod:`meshnet_node.runtime_pin`.
|
||||
|
||||
The three label fields are diagnosis only and are not digested.
|
||||
"""
|
||||
@@ -419,7 +476,7 @@ class RuntimeRecipe:
|
||||
_require_int(value, f"recipe.{axis}", 1)
|
||||
else:
|
||||
_require_text(value, f"recipe.{axis}")
|
||||
_require_pin(self.tokenizer_revision, "recipe.tokenizer_revision")
|
||||
_require_tokenizer_identity(self.tokenizer_revision, "recipe.tokenizer_revision")
|
||||
_require_runtime_pin(self.runtime_version, self.backend_id)
|
||||
_require_text(self.recipe_id, "recipe.recipe_id")
|
||||
_require_text(self.recipe_version, "recipe.recipe_version")
|
||||
|
||||
Reference in New Issue
Block a user