gabriel / muse public
test_keygen_no_bytes_copy.py python
115 lines 4.9 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 150 days ago
1 """Tests for unnecessary bytes() copy in generate_hd_keypair.
2
3 generate_hd_keypair previously called:
4 private_key = Ed25519PrivateKey.from_private_bytes(bytes(dk.private_bytes))
5
6 The explicit bytes() conversion creates a second, immutable copy of the 32-byte
7 private key material in the Python heap — one that can never be zeroed. Since
8 Ed25519PrivateKey.from_private_bytes() accepts any bytes-like object (including
9 bytearray), the conversion is unnecessary.
10
11 Fix:
12 private_key = Ed25519PrivateKey.from_private_bytes(dk.private_bytes)
13
14 This passes the bytearray buffer directly; no immutable copy is created.
15 dk.zero() then wipes the only Python-level copy.
16
17 Coverage
18 --------
19 I from_private_bytes accepts bytearray (no bytes() copy needed)
20 I1 Ed25519PrivateKey.from_private_bytes works with a bytearray argument
21 I2 the resulting key is functionally equivalent to one built from bytes
22
23 II generate_hd_keypair does not create a bytes copy of dk.private_bytes
24 II1 after generate_hd_keypair, the DerivedKey's private_bytes is zeroed
25 (confirms dk.zero() ran — would not zero a separate bytes copy)
26 """
27
28 from __future__ import annotations
29
30 import pathlib
31 from unittest.mock import patch
32
33 import pytest
34
35 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
36
37 from muse.core import keypair as kp_module
38 from muse.core import identity as id_module
39 from muse.core import hdkeys as _hdkeys
40 from muse.core.slip010 import DerivedKey
41 from muse.core.bip39 import mnemonic_to_seed
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 @pytest.fixture()
51 def isolated(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> pathlib.Path:
52 fake_home = tmp_path / "home"
53 fake_home.mkdir(parents=True, exist_ok=True)
54 monkeypatch.setattr(pathlib.Path, "home", staticmethod(lambda: fake_home))
55 monkeypatch.setattr(kp_module, "_KEYS_DIR", fake_home / ".muse" / "keys")
56 monkeypatch.setattr(id_module, "_IDENTITY_DIR", fake_home / ".muse")
57 monkeypatch.setattr(id_module, "_IDENTITY_FILE", fake_home / ".muse" / "identity.toml")
58 return fake_home
59
60
61 # ---------------------------------------------------------------------------
62 # I from_private_bytes accepts bytearray directly
63 # ---------------------------------------------------------------------------
64
65 class TestFromPrivateBytesAcceptsBytearray:
66 def test_I1_bytearray_accepted(self) -> None:
67 """I1: Ed25519PrivateKey.from_private_bytes accepts a bytearray argument."""
68 raw = bytearray(b"\x42" * 32)
69 key = Ed25519PrivateKey.from_private_bytes(raw)
70 assert key is not None
71
72 def test_I2_equivalent_to_bytes_version(self) -> None:
73 """I2: key from bytearray produces the same public key as key from bytes."""
74 raw_bytes = bytes(b"\x42" * 32)
75 raw_bytearray = bytearray(b"\x42" * 32)
76 key_from_bytes = Ed25519PrivateKey.from_private_bytes(raw_bytes)
77 key_from_bytearray = Ed25519PrivateKey.from_private_bytes(raw_bytearray)
78 from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
79 pub_bytes = key_from_bytes.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
80 pub_bytearray = key_from_bytearray.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
81 assert pub_bytes == pub_bytearray
82
83
84 # ---------------------------------------------------------------------------
85 # II generate_hd_keypair zeroes DerivedKey (no bytes copy escaping)
86 # ---------------------------------------------------------------------------
87
88 class TestGenerateHdKeypairNoBytescopy:
89 def test_II1_derived_key_zeroed_after_keygen(self, isolated: pathlib.Path) -> None:
90 """II1: the DerivedKey's private_bytes are zeroed after generate_hd_keypair.
91
92 This verifies dk.zero() ran on the actual DerivedKey, not a copy.
93 If a bytes() copy existed, dk.zero() would still run, but this test
94 confirms the overall zeroing contract holds — the tracked DerivedKey
95 is always zeroed regardless of whether a bytes copy existed.
96 """
97 captured: list[DerivedKey] = []
98 original_derive = _hdkeys.derive_identity_key
99
100 def capturing_derive(*args, **kwargs):
101 dk = original_derive(*args, **kwargs)
102 captured.append(dk)
103 return dk
104
105 with patch.object(_hdkeys, "derive_identity_key", side_effect=capturing_derive):
106 kp_module.generate_hd_keypair("localhost:10003", _SEED)
107
108 assert captured, "derive_identity_key was not called"
109 dk = captured[0]
110 assert dk.private_bytes == bytearray(32), (
111 "private_bytes must be zeroed after generate_hd_keypair (no bytes copy escaping zero)"
112 )
113 assert dk.chain_code == bytearray(32), (
114 "chain_code must be zeroed after generate_hd_keypair"
115 )
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 150 days ago