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:
@@ -21,15 +21,21 @@ from meshnet_node.runtime_pin import (
|
||||
RuntimePinError,
|
||||
load_runtime_pin,
|
||||
)
|
||||
from meshnet_node.runtime_recipe import RecipeIdentityError, RuntimeRecipe
|
||||
from meshnet_node.runtime_recipe import (
|
||||
RecipeIdentityError,
|
||||
RuntimeRecipe,
|
||||
tokenizer_identity,
|
||||
)
|
||||
from meshnet_tracker.recipe import (
|
||||
RecipeIdentityError as TrackerRecipeIdentityError,
|
||||
parse_identity,
|
||||
tokenizer_identity as tracker_tokenizer_identity,
|
||||
)
|
||||
|
||||
REPO_LOCK_DIR = (
|
||||
Path(__file__).resolve().parent.parent / "packages" / "node" / "native" / "llama"
|
||||
)
|
||||
TOKENIZER = tokenizer_identity({"tokenizer.json": b'{"vocab":{"a":0}}\n'})
|
||||
|
||||
|
||||
def _recipe(**changes: object) -> RuntimeRecipe:
|
||||
@@ -39,10 +45,14 @@ def _recipe(**changes: object) -> RuntimeRecipe:
|
||||
"compute_dtype": "float32",
|
||||
"kv_dtype": "q8_0",
|
||||
"kv_layout": "paged-v1",
|
||||
"tokenizer_revision": "0123456789abcdef",
|
||||
"tokenizer_revision": TOKENIZER,
|
||||
"architecture_adapter": "llama/range-v1",
|
||||
"backend_id": "llama.cpp",
|
||||
"runtime_version": "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64,
|
||||
"runtime_version": (
|
||||
"llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64
|
||||
+ "+build." + "f" * 64
|
||||
+ "+artifact." + "a" * 64
|
||||
),
|
||||
"recipe_id": "example-gguf",
|
||||
"recipe_version": "1",
|
||||
"catalogue_version": "2026.07.1",
|
||||
@@ -62,6 +72,8 @@ def _write_fixture_workspace(
|
||||
schema_version: int = 1,
|
||||
upstream: str = "https://github.com/ggml-org/llama.cpp.git",
|
||||
upstream_commit_file: str | None = None,
|
||||
patched_tree: str = "a" * 40,
|
||||
build: dict[str, object] | None = None,
|
||||
) -> Path:
|
||||
"""A minimal DGR-027-shaped lock workspace; overrides create disagreement."""
|
||||
if patches is None:
|
||||
@@ -85,6 +97,8 @@ def _write_fixture_workspace(
|
||||
"schema_version": schema_version,
|
||||
"upstream": upstream,
|
||||
"commit": commit,
|
||||
"patched_tree": patched_tree,
|
||||
"build": build or {"configure_flags": ["-DTEST=ON"]},
|
||||
"patch_series": names if lock_series is None else lock_series,
|
||||
}
|
||||
),
|
||||
@@ -124,18 +138,19 @@ def test_committed_manifest_derives_a_deterministic_runtime_pin():
|
||||
)
|
||||
assert pin.upstream_commit == lock["commit"]
|
||||
assert list(pin.patch_series) == lock["patch_series"]
|
||||
# The axis value names the runtime, the exact commit, and the stack digest,
|
||||
# so changing any of the three changes every downstream recipe digest.
|
||||
# The axis value names the runtime, exact commit, stack, and build recipe.
|
||||
assert pin.runtime_version == (
|
||||
f"llama.cpp@{lock['commit']}+patchstack.{pin.patch_stack_digest}"
|
||||
f"+build.{pin.build_recipe_digest}"
|
||||
)
|
||||
assert len(pin.patch_stack_digest) == 64
|
||||
|
||||
|
||||
def test_derived_axis_value_is_a_valid_recipe_pin():
|
||||
pin = load_runtime_pin(REPO_LOCK_DIR)
|
||||
recipe = _recipe(runtime_version=pin.runtime_version)
|
||||
assert recipe.runtime_version == pin.runtime_version
|
||||
runtime_version = pin.runtime_version + "+artifact." + "a" * 64
|
||||
recipe = _recipe(runtime_version=runtime_version)
|
||||
assert recipe.runtime_version == runtime_version
|
||||
assert len(recipe.runtime_recipe_digest) == 64
|
||||
|
||||
|
||||
@@ -179,6 +194,16 @@ def test_patch_order_is_part_of_the_stack_identity(tmp_path):
|
||||
assert forward.patch_stack_digest != swapped.patch_stack_digest
|
||||
|
||||
|
||||
def test_build_recipe_change_changes_runtime_identity(tmp_path):
|
||||
baseline = load_runtime_pin(_write_fixture_workspace(tmp_path / "a"))
|
||||
changed = load_runtime_pin(
|
||||
_write_fixture_workspace(
|
||||
tmp_path / "b", build={"configure_flags": ["-DTEST=OFF"]}
|
||||
)
|
||||
)
|
||||
assert baseline.runtime_version != changed.runtime_version
|
||||
|
||||
|
||||
# --- every manifest disagreement fails closed ------------------------------
|
||||
|
||||
|
||||
@@ -257,6 +282,53 @@ def test_empty_patch_series_requires_empty_series_files(tmp_path):
|
||||
load_runtime_pin(tmp_path)
|
||||
|
||||
|
||||
# --- tokenizer identities are bytes, never labels -------------------------
|
||||
|
||||
|
||||
def _vector_identity() -> dict[str, object]:
|
||||
vectors = json.loads(
|
||||
(Path(__file__).parent / "data" / "recipe_fingerprint_vectors.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
return json.loads(json.dumps(vectors["vectors"][0]["identity"]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("moving", ["origin/main", "stable", "release", "v1", "HEAD"])
|
||||
def test_node_rejects_every_label_as_a_tokenizer_identity(moving):
|
||||
with pytest.raises(RecipeIdentityError, match="content-addressed tokenizer"):
|
||||
_recipe(tokenizer_revision=moving)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("moving", ["origin/main", "stable", "release", "v1", "HEAD"])
|
||||
def test_tracker_rejects_every_label_as_a_tokenizer_identity(moving):
|
||||
doc = _vector_identity()
|
||||
doc["recipe"]["tokenizer_revision"] = moving # type: ignore[index]
|
||||
doc.pop("fingerprint", None)
|
||||
with pytest.raises(TrackerRecipeIdentityError, match="content-addressed"):
|
||||
parse_identity(doc)
|
||||
|
||||
|
||||
def test_node_and_tracker_independently_derive_identical_tokenizer_bytes_identity():
|
||||
files = {
|
||||
"tokenizer.json": b'{"vocab":{"a":0}}\n',
|
||||
"tokenizer_config.json": b'{"bos_token":"<s>"}\n',
|
||||
}
|
||||
assert tokenizer_identity(files) == tracker_tokenizer_identity(files)
|
||||
|
||||
|
||||
def test_tokenizer_bytes_under_one_label_and_one_byte_change_get_new_recipe_fingerprints():
|
||||
label = "stable"
|
||||
first = {"tokenizer.json": b'{"vocab":{"a":0}}\n'}
|
||||
second = {"tokenizer.json": b'{"vocab":{"a":1}}\n'}
|
||||
first_id = tokenizer_identity(first)
|
||||
second_id = tokenizer_identity(second)
|
||||
assert first_id != second_id, label
|
||||
assert _recipe(tokenizer_revision=first_id).runtime_recipe_digest != _recipe(
|
||||
tokenizer_revision=second_id
|
||||
).runtime_recipe_digest
|
||||
|
||||
|
||||
# --- both identity implementations reject a moving runtime -----------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user