"""Stage 9 acceptance: a protected scene package, opened from the application. The container could always read a signed or sealed package, but the application never passed a verification key or a content-key provider, so a legitimate protected release was refused with "this package is signed; a verification key is required" — and the only ways to run it were to ship a plain package or to disable the check. These tests are about the *wiring*: preferences → ``SceneKeys`` → the loader the pane actually uses. """ from __future__ import annotations import json from pathlib import Path import pytest from orisys_app_shared.scene3d import protect, synthetic from orisys_app_shared.scene3d.contracts import ExpectedGeometry, ThreeDError from orisys_app_shared.scene3d.package import write_package from orisys_studio.scene3d.asset_source import SceneSource, load_package from orisys_studio.scene3d.keys import ( PREF_CONTENT_KEY_FILE, PREF_CONTENT_KEY_ID, PREF_REQUIRE_SIGNATURE, PREF_VERIFY_KEY, resolve_scene_keys, ) KEY_ID = "finger-display" CONTENT_KEY_HEX = "11" * 32 def _expected(scene) -> ExpectedGeometry: return ExpectedGeometry( "finger", "", scene.geometry_id, scene.field_domain_id, scene.shape_hw ) def _keys_and_paths(tmp_path: Path): """A signing key pair and a content key file, as a release build would have.""" private = protect.generate_signing_key() public = protect.public_key_pem(private) verify_path = tmp_path / "verify.pem" verify_path.write_bytes(public) key_path = tmp_path / "content.key" key_path.write_text(CONTENT_KEY_HEX + "\n", encoding="utf-8") provider = protect.MappingKeyProvider({KEY_ID: CONTENT_KEY_HEX}) return private, public, verify_path, key_path, provider def _write_signed(tmp_path: Path, scene, *, sealed: bool): private, public, verify_path, key_path, provider = _keys_and_paths(tmp_path) from orisys_app_shared.scene3d.contracts import Sealing target = tmp_path / ("sealed.oryscene" if sealed else "signed.oryscene") write_package( scene, target, sealing=Sealing( key_id=KEY_ID if sealed else "", provider=provider if sealed else None, signing_key_pem=private, signing_key_reference="stage-9", verify_key_pem=public, ), ) return target, verify_path, key_path def _source(path: Path) -> SceneSource: return SceneSource(path=path, kind="package", directory=path.parent) @pytest.mark.parametrize("sealed", (False, True), ids=("signed", "sealed")) def test_a_protected_package_opens_from_the_application(tmp_path, sealed): """The review's R4: the library could read it; the application could not. With keys resolved from preferences, the pane's own loader accepts both signed and sealed release packages and returns their scene intact. """ scene = synthetic.poc_scene() package, verify_path, key_path = _write_signed(tmp_path, scene, sealed=sealed) prefs = {PREF_VERIFY_KEY: str(verify_path)} if sealed: prefs.update( { PREF_CONTENT_KEY_FILE: str(key_path), PREF_CONTENT_KEY_ID: KEY_ID, } ) keys = resolve_scene_keys(prefs) described = keys.describe() assert keys.verify_key_pem is not None assert described["verify_key"] is True assert CONTENT_KEY_HEX.encode() not in keys.verify_key_pem if sealed: assert keys.provider is not None assert described["content_key"] is True assert described["content_key_id"] == KEY_ID # The description carries paths and ids, never key material. assert CONTENT_KEY_HEX not in json.dumps(described) assets = load_package(_source(package), _expected(scene), keys) assert assets.geometry_id == scene.geometry_id # The writer replaces the synthetic scene's placeholder with the manifest hash. assert len(assets.asset_hash) == 64 def test_without_the_verification_key_a_signed_package_is_refused(tmp_path): """Not a silent fallback to plain: the refusal names what is missing.""" scene = synthetic.poc_scene() package, _verify_path, _key_path = _write_signed(tmp_path, scene, sealed=False) keys = resolve_scene_keys({}) with pytest.raises(ThreeDError) as excinfo: load_package(_source(package), _expected(scene), keys) assert excinfo.value.diagnostic.code == "asset_auth_failed" assert "verification key" in excinfo.value.diagnostic.message def test_a_missing_or_unreadable_key_file_is_a_state_not_a_crash(tmp_path): """The 2D application must start whatever the key configuration looks like.""" keys = resolve_scene_keys( { PREF_VERIFY_KEY: str(tmp_path / "gone.pem"), PREF_CONTENT_KEY_FILE: str(tmp_path / "gone.key"), PREF_CONTENT_KEY_ID: KEY_ID, } ) assert keys.verify_key_pem is None and keys.provider is None described = keys.describe() assert described["verify_key_error"] == "missing" assert described["content_key_error"] == "missing" # A key file without a key id is not usable either, and says which half is absent. key_path = tmp_path / "content.key" key_path.write_text(CONTENT_KEY_HEX, encoding="utf-8") keys = resolve_scene_keys({PREF_CONTENT_KEY_FILE: str(key_path)}) assert keys.provider is None assert keys.describe()["content_key_error"] == "no key id configured" def test_release_mode_refuses_an_unsigned_package(tmp_path): """The gate that replaces "ask the engineer to sign it".""" scene = synthetic.poc_scene() plain = tmp_path / "plain.oryscene" write_package(scene, plain) release = resolve_scene_keys({PREF_REQUIRE_SIGNATURE: True}) assert release.require_signature is True with pytest.raises(ThreeDError) as excinfo: load_package(_source(plain), _expected(scene), release) assert excinfo.value.diagnostic.code == "asset_auth_failed" assert "requires signed scene packages" in excinfo.value.diagnostic.message # The same package is fine in a development build (and the shipped engineering # package is exactly this case). dev = resolve_scene_keys({}) assert load_package(_source(plain), _expected(scene), dev).geometry_id == scene.geometry_id class _InlineExecutor: """Runs submitted work immediately: the loader's path without a thread.""" def submit(self, function, *args, **kwargs): from concurrent.futures import Future future: Future = Future() try: future.set_result(function(*args, **kwargs)) except BaseException as exc: # noqa: BLE001 - mirrored into the future future.set_exception(exc) return future def shutdown(self, wait: bool = True) -> None: # noqa: ARG002 - executor contract return None def test_the_scene_loader_passes_the_keys_to_its_worker(qt_app, tmp_path): """The pane's loader is where the wiring has to happen, not just ``load_package``.""" from orisys_studio.scene3d.asset_source import SceneAssetLoader scene = synthetic.poc_scene() package, verify_path, _key_path = _write_signed(tmp_path, scene, sealed=False) keys = resolve_scene_keys({PREF_VERIFY_KEY: str(verify_path)}) loader = SceneAssetLoader() loader._executor = _InlineExecutor() loaded: list = [] failures: list = [] loader.loaded.connect( lambda assets, description, request_id, device: loaded.append(assets) ) loader.failed.connect(lambda item, request_id: failures.append(item)) try: loader.load(_source(package), _expected(scene), keys=keys) assert not failures, [item.message for item in failures] assert loaded and loaded[0].geometry_id == scene.geometry_id finally: loader.close() # Without them the same file is refused, and the loader reports it rather than # raising into the GUI thread. loader = SceneAssetLoader() loader._executor = _InlineExecutor() failures = [] loader.failed.connect(lambda item, request_id: failures.append(item)) try: loader.load(_source(package), _expected(scene), keys=resolve_scene_keys({})) assert failures and failures[0].code == "asset_auth_failed" finally: loader.close() def test_the_pane_carries_the_keys_into_its_loader(qt_app): from orisys_studio.ui.mesh3d_panel import Mesh3dPanel panel = Mesh3dPanel() keys = resolve_scene_keys({}) assert panel.keys() is None panel.set_keys(keys) assert panel.keys() is keys