"""Tests for DerivedKey zeroing in get_signing_identity (MUSE_AGENT_KEY_FD path). get_signing_identity reads a 64-byte sub-seed from MUSE_AGENT_KEY_FD, calls derive_identity_key() to produce a DerivedKey, creates the Ed25519 private key from it, then zeros the sub_seed bytearray. But it did NOT call dk.zero() — leaving the DerivedKey's 32-byte private_bytes and chain_code in heap memory indefinitely. Fix: - Call dk.zero() immediately after Ed25519PrivateKey.from_private_bytes(). - The Ed25519 key object holds the key material; the DerivedKey is no longer needed and must be wiped. Coverage -------- I DerivedKey is zeroed after get_signing_identity returns (MUSE_AGENT_KEY_FD) I1 private_bytes is all-zero in the DerivedKey after signing identity resolved I2 chain_code is all-zero in the DerivedKey after signing identity resolved """ from __future__ import annotations import os from unittest.mock import patch import pytest from muse.core import hdkeys as _hdkeys from muse.core.slip010 import DerivedKey from muse.core.bip39 import mnemonic_to_seed from muse.core.hdkeys import derive_agent_sub_seed, DOMAIN_IDENTITY from muse.cli import config as config_mod _MNEMONIC = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon about" ) _SEED = mnemonic_to_seed(_MNEMONIC) _SUB_SEED = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0) class TestAgentKeyFdDerivedKeyZeroed: def _make_fd(self) -> int: """Create a pipe and write 64 bytes of sub-seed; return the read end fd.""" r, w = os.pipe() os.write(w, bytes(_SUB_SEED)) os.close(w) return r def test_I1_private_bytes_zeroed_after_fd_read(self, monkeypatch: pytest.MonkeyPatch) -> None: """I1: DerivedKey.private_bytes is zeroed after the signing identity is resolved.""" captured: list[DerivedKey] = [] original_derive = _hdkeys.derive_identity_key def capturing_derive(*args: int | bytes, **kwargs: int) -> DerivedKey: dk = original_derive(*args, **kwargs) captured.append(dk) return dk r_fd = self._make_fd() monkeypatch.setenv("MUSE_AGENT_KEY_FD", str(r_fd)) monkeypatch.setenv("MUSE_AGENT_HANDLE", "test-agent") with patch.object(_hdkeys, "derive_identity_key", side_effect=capturing_derive): config_mod.get_signing_identity() assert captured, "derive_identity_key was not called via MUSE_AGENT_KEY_FD path" dk = captured[0] assert dk.private_bytes == bytearray(32), ( "DerivedKey.private_bytes must be zeroed after get_signing_identity returns" ) def test_I2_chain_code_zeroed_after_fd_read(self, monkeypatch: pytest.MonkeyPatch) -> None: """I2: DerivedKey.chain_code is zeroed after the signing identity is resolved.""" captured: list[DerivedKey] = [] original_derive = _hdkeys.derive_identity_key def capturing_derive(*args: int | bytes, **kwargs: int) -> DerivedKey: dk = original_derive(*args, **kwargs) captured.append(dk) return dk r_fd = self._make_fd() monkeypatch.setenv("MUSE_AGENT_KEY_FD", str(r_fd)) monkeypatch.setenv("MUSE_AGENT_HANDLE", "test-agent") with patch.object(_hdkeys, "derive_identity_key", side_effect=capturing_derive): config_mod.get_signing_identity() assert captured, "derive_identity_key was not called via MUSE_AGENT_KEY_FD path" dk = captured[0] assert dk.chain_code == bytearray(32), ( "DerivedKey.chain_code must be zeroed after get_signing_identity returns" )