gabriel / muse public
test_agent_sub_seed_zeroing.py python
129 lines 5.2 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Tests for derive_agent_sub_seed memory zeroing.
2
3 derive_agent_sub_seed manually chains master_key → child_key × 4 without
4 zeroing any intermediate DerivedKey, and returns the final key material as
5 immutable bytes. This means:
6
7 1. Four intermediate DerivedKey objects (each with 32-byte private_bytes
8 and chain_code) linger in the Python heap after reassignment.
9 2. The returned 64-byte bytearray lives in the caller's frame until
10 manually zeroed — callers must now do this.
11
12 Fix:
13 - Zero each intermediate DerivedKey before reassigning dk.
14 - Zero the final DerivedKey after building the return value.
15 - Return bytearray so callers can zero it after use.
16
17 Coverage
18 --------
19 I Return type is bytearray
20 I1 derive_agent_sub_seed returns bytearray, not bytes
21
22 II Callers can zero the returned value
23 II1 returned bytearray can be overwritten in-place
24
25 III Intermediate keys are zeroed (via monkey-patch inspection)
26 III1 all DerivedKey objects created during derivation are zeroed
27 by the time the function returns
28
29 IV Correctness unaffected
30 IV1 same inputs → same 64-byte output (determinism preserved)
31 IV2 output length is always 64 bytes
32 """
33
34 from __future__ import annotations
35
36 from unittest.mock import patch, call
37 import pytest
38
39 from muse.core.bip39 import mnemonic_to_seed
40 from muse.core.hdkeys import derive_agent_sub_seed, DOMAIN_IDENTITY
41 from muse.core.slip010 import DerivedKey
42
43 _MNEMONIC = (
44 "abandon abandon abandon abandon abandon abandon abandon abandon "
45 "abandon abandon abandon about"
46 )
47 _SEED = mnemonic_to_seed(_MNEMONIC)
48
49
50 # ---------------------------------------------------------------------------
51 # I Return type is bytearray
52 # ---------------------------------------------------------------------------
53
54 class TestReturnType:
55 def test_I1_returns_bytearray(self) -> None:
56 """I1: derive_agent_sub_seed must return bytearray so callers can zero it."""
57 result = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
58 assert isinstance(result, bytearray), (
59 f"Expected bytearray, got {type(result).__name__}"
60 )
61
62
63 # ---------------------------------------------------------------------------
64 # II Callers can zero the returned value
65 # ---------------------------------------------------------------------------
66
67 class TestCallerCanZero:
68 def test_II1_returned_bytearray_is_mutable(self) -> None:
69 """II1: the returned bytearray can be overwritten in place."""
70 result = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
71 assert any(b != 0 for b in result), "pre-condition: result must not already be zero"
72 result[:] = b"\x00" * len(result)
73 assert result == bytearray(64), "caller must be able to zero the returned bytearray"
74
75
76 # ---------------------------------------------------------------------------
77 # III Intermediate keys are zeroed
78 # ---------------------------------------------------------------------------
79
80 class TestIntermediatesZeroed:
81 def test_III1_all_derived_keys_zeroed_on_return(self) -> None:
82 """III1: every DerivedKey created during derivation is zeroed by the time
83 derive_agent_sub_seed returns."""
84 import muse.core.hdkeys as hdkeys_mod
85 from muse.core.slip010 import master_key as real_master, child_key as real_child
86
87 created: list[DerivedKey] = []
88
89 def tracking_master(seed: bytes) -> DerivedKey:
90 dk = real_master(seed)
91 created.append(dk)
92 return dk
93
94 def tracking_child(parent: DerivedKey, index: int) -> DerivedKey:
95 dk = real_child(parent, index)
96 created.append(dk)
97 return dk
98
99 # Patch in the hdkeys namespace where the names are bound
100 with patch.object(hdkeys_mod, "master_key", side_effect=tracking_master), \
101 patch.object(hdkeys_mod, "child_key", side_effect=tracking_child):
102 derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
103
104 assert created, "No DerivedKey objects were tracked — something is wrong"
105 for i, dk in enumerate(created):
106 assert dk.private_bytes == bytearray(32), (
107 f"DerivedKey #{i} private_bytes not zeroed after derive_agent_sub_seed"
108 )
109 assert dk.chain_code == bytearray(32), (
110 f"DerivedKey #{i} chain_code not zeroed after derive_agent_sub_seed"
111 )
112
113
114 # ---------------------------------------------------------------------------
115 # IV Correctness unaffected
116 # ---------------------------------------------------------------------------
117
118 class TestCorrectness:
119 def test_IV1_deterministic(self) -> None:
120 """IV1: same inputs always produce the same 64 bytes."""
121 r1 = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
122 r2 = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
123 # Compare before zeroing
124 assert bytes(r1) == bytes(r2), "derive_agent_sub_seed must be deterministic"
125
126 def test_IV2_length_64(self) -> None:
127 """IV2: output is always exactly 64 bytes."""
128 result = derive_agent_sub_seed(_SEED, domain=DOMAIN_IDENTITY, agent_id=0)
129 assert len(result) == 64
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago