Files
orisys-app/apps/studio/tests/test_scene3d_protected_package.py
2026-09-27 19:20:43 +08:00

221 lines
8.4 KiB
Python

"""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