"""Full chain tests for resolve_signing_identity. Verifies the complete keychain → derive → sign → verify path with no disk reads from ~/.muse/keys/. Chain invariants ---------------- KC-1 keychain → mnemonic_to_seed → derive_path → Ed25519PrivateKey — full chain produces a usable signing key. KC-2 Derived key signs a canonical message verifiable with the stored public key. KC-3 No mnemonic in keychain → returns None gracefully. KC-4 No identity entry for hub → returns None gracefully. KC-5 Identity entry missing hd_path → returns None gracefully. KC-6 Same mnemonic + same hd_path → always the same key (deterministic). KC-7 Different hd_paths → different keys. """ from __future__ import annotations import pathlib import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from muse.core.identity import save_identity, resolve_signing_identity from muse.core.keypair import derive_hd_public_info from muse.core.bip39 import mnemonic_to_seed _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) _HUB = "https://localhost:1337" _HD_PATH = "m/1075233755'/0'/0'/0'/0'/0'" _AGENT_HD_PATH = "m/1075233755'/1'/0'/0'/0'/0'" # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def isolated_env( monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path ) -> pathlib.Path: import muse.core.keypair as kp_module import muse.core.identity as id_module fake_home = tmp_path / "home" fake_home.mkdir(parents=True, exist_ok=True) monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home)) monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys") monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse") monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml") return fake_home @pytest.fixture() def keychain_mnemonic(monkeypatch: pytest.MonkeyPatch) -> None: import muse.core.keychain as kc monkeypatch.setattr(kc, "is_available", lambda: True) monkeypatch.setattr(kc, "load", lambda: _MNEMONIC) def _save_human_entry(fingerprint: str) -> None: save_identity(_HUB, { "type": "human", "handle": "gabriel", "algorithm": "ed25519", "fingerprint": fingerprint, "hd_path": _HD_PATH, }) def _fingerprint() -> str: seed = mnemonic_to_seed(_MNEMONIC) _, fp = derive_hd_public_info(seed) return fp # --------------------------------------------------------------------------- # KC-1 — full chain returns a private key # --------------------------------------------------------------------------- class TestFullChain: def test_KC_1_returns_signing_identity( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-1: full chain returns (handle, Ed25519PrivateKey).""" _save_human_entry(_fingerprint()) result = resolve_signing_identity(_HUB) assert result is not None, "resolve_signing_identity returned None" handle, private_key = result assert handle == "gabriel" assert isinstance(private_key, Ed25519PrivateKey) def test_KC_2_derived_key_signs_verifiable_message( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-2: the derived key produces a signature verifiable with stored pubkey.""" from muse.core.slip010 import derive_path, to_ed25519_private_key from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat seed = mnemonic_to_seed(_MNEMONIC) dk = derive_path(seed, _HD_PATH) expected_key = to_ed25519_private_key(dk) dk.zero() expected_pub = expected_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) _save_human_entry(_fingerprint()) result = resolve_signing_identity(_HUB) assert result is not None _, private_key = result message = b"test canonical message" sig = private_key.sign(message) derived_pub = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) assert derived_pub == expected_pub, "Derived public key does not match expected" # Verify signature with expected public key expected_key.public_key().verify(sig, message) # raises if invalid # --------------------------------------------------------------------------- # KC-3 to KC-5 — graceful None returns # --------------------------------------------------------------------------- class TestGracefulNone: def test_KC_3_no_mnemonic_returns_none( self, isolated_env: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: """KC-3: no mnemonic in keychain → None.""" import muse.core.keychain as kc monkeypatch.setattr(kc, "is_available", lambda: True) monkeypatch.setattr(kc, "load", lambda: None) _save_human_entry(_fingerprint()) assert resolve_signing_identity(_HUB) is None def test_KC_4_no_identity_entry_returns_none( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-4: no identity.toml entry for hub → None.""" assert resolve_signing_identity(_HUB) is None def test_KC_5_no_hd_path_returns_none( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-5: identity entry missing hd_path → None.""" save_identity(_HUB, { "type": "human", "handle": "gabriel", "algorithm": "ed25519", "fingerprint": _fingerprint(), }) assert resolve_signing_identity(_HUB) is None # --------------------------------------------------------------------------- # KC-6 — deterministic derivation # --------------------------------------------------------------------------- class TestDeterministic: def test_KC_6_same_mnemonic_same_key( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-6: same mnemonic + hd_path always produces the same public key bytes.""" from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat _save_human_entry(_fingerprint()) r1 = resolve_signing_identity(_HUB) r2 = resolve_signing_identity(_HUB) assert r1 is not None and r2 is not None pub1 = r1[1].public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) pub2 = r2[1].public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) assert pub1 == pub2, "Derivation is not deterministic" def test_KC_7_different_hd_paths_different_keys( self, isolated_env: pathlib.Path, keychain_mnemonic: None, ) -> None: """KC-7: different hd_paths → different public keys.""" from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from muse.core.slip010 import derive_path, to_ed25519_private_key seed = mnemonic_to_seed(_MNEMONIC) dk1 = derive_path(seed, _HD_PATH) key1 = to_ed25519_private_key(dk1) dk1.zero() dk2 = derive_path(seed, _AGENT_HD_PATH) key2 = to_ed25519_private_key(dk2) dk2.zero() pub1 = key1.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) pub2 = key2.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) assert pub1 != pub2, "Different HD paths must produce different keys"