diff --git a/backend/tests/test_delivery.py b/backend/tests/test_delivery.py index 66b9a9a..8c6f1e8 100644 --- a/backend/tests/test_delivery.py +++ b/backend/tests/test_delivery.py @@ -45,6 +45,18 @@ node_observation = importlib.import_module("scripts.release.node_observation") installer = importlib.import_module("scripts.release.install") +@pytest.mark.parametrize("field", ["release_id", "configuration_id", "manifest_sha256"]) +def test_activation_refuses_checked_binding_that_differs_from_explicit_request(monkeypatch, field): + monkeypatch.setattr(installer.os, "geteuid", lambda: 0) + monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) + release, batch, expected = "sd-123456789abc", "cfg-" + "1" * 24, "1" * 64 + binding = {"release_id": release, "configuration_id": batch, "manifest_sha256": expected} + binding[field] = "different" + monkeypatch.setattr(configuration, "check_installed", lambda _: binding) + with pytest.raises(policy.DeliveryError, match="configuration not bound"): + installer.activate(release, expected, batch) + + @pytest.mark.parametrize("mode,owner", [(stat.S_IFREG, 0), (stat.S_IFSOCK, 1000)]) def test_system_bus_preflight_refuses_unprotected_endpoint(monkeypatch, mode, owner): monkeypatch.setattr(installer.preflight, "protected", lambda *args, **kwargs: None) diff --git a/scripts/release/install.py b/scripts/release/install.py index 97d5d06..3aea81f 100644 --- a/scripts/release/install.py +++ b/scripts/release/install.py @@ -460,16 +460,19 @@ def activate(release_id: str, expected: str, batch: str) -> dict: or not re.fullmatch(r"cfg-[0-9a-f]{24}", batch) ): raise DeliveryError("invalid activation identity or privilege") + expected = sha256(expected) release = RELEASES / release_id preflight.protected(release, directory=True) - data = manifest.verify(release, expected) from .configuration import check_installed, installed_receipt_path config_receipt = installed_receipt_path(release_id, batch) binding = check_installed(config_receipt) + # check_installed has already verified the complete selected release and + # configuration bytes. Bind that result to the explicit operator request. if ( binding.get("manifest_sha256") != expected - or binding.get("source_sha") != data["source_sha"] + or binding.get("release_id") != release_id + or binding.get("configuration_id") != batch ): raise DeliveryError("configuration not bound to selected release") require_stopped() @@ -512,7 +515,7 @@ def activate(release_id: str, expected: str, batch: str) -> dict: # mismatch. No service is started and check-active refuses that state. selection() return { - "selected_source_sha": data["source_sha"], + "selected_source_sha": binding["source_sha"], "configuration_id": batch, "activated": True, "services_started": False,