diff --git a/sdk/js/src/__tests__/solana.test.ts b/sdk/js/src/__tests__/solana.test.ts index ee908a900..07a866e50 100644 --- a/sdk/js/src/__tests__/solana.test.ts +++ b/sdk/js/src/__tests__/solana.test.ts @@ -80,5 +80,18 @@ describe('solana support', () => { consoleSpy.mockRestore() }) + + it('should hash the full key material for GetKeyResponse (regression: previously fell through unhashed)', async () => { + const client = new DstackClient() + const result = await client.getKey('/', 'test') + + const legacyKeypair = toKeypair(result) + const secureKeypair = toKeypairSecure(result) + + // toKeypairSecure must apply SHA256 to the full key material, as its + // docstring promises. If it silently falls through to the raw key (the + // bug this test guards against), both public keys are identical. + expect(secureKeypair.publicKey.toBase58()).not.toBe(legacyKeypair.publicKey.toBase58()) + }) }) }) diff --git a/sdk/js/src/__tests__/viem.test.ts b/sdk/js/src/__tests__/viem.test.ts index 307ef49d4..db3d88bce 100644 --- a/sdk/js/src/__tests__/viem.test.ts +++ b/sdk/js/src/__tests__/viem.test.ts @@ -90,5 +90,18 @@ describe('viem support', () => { consoleSpy.mockRestore() }) + + it('should hash the full key material for GetKeyResponse (regression: previously fell through unhashed)', async () => { + const client = new DstackClient() + const result = await client.getKey('/', 'test') + + const legacyAccount = toViemAccount(result) + const secureAccount = toViemAccountSecure(result) + + // toViemAccountSecure must apply SHA256 to the full key material, as its + // docstring promises. If it silently falls through to the raw key (the + // bug this test guards against), both addresses are identical. + expect(secureAccount.address).not.toBe(legacyAccount.address) + }) }) }) diff --git a/sdk/js/src/solana.ts b/sdk/js/src/solana.ts index 5fe69f633..447dc254a 100644 --- a/sdk/js/src/solana.ts +++ b/sdk/js/src/solana.ts @@ -32,5 +32,5 @@ export function toKeypairSecure(keyResponse: GetTlsKeyResponse | GetKeyResponse) const buf = sha256(keyResponse.asUint8Array()) return Keypair.fromSeed(buf) } - return Keypair.fromSeed(keyResponse.key) + return Keypair.fromSeed(sha256(keyResponse.key)) } diff --git a/sdk/js/src/viem.ts b/sdk/js/src/viem.ts index d2b505624..a1323b1a3 100644 --- a/sdk/js/src/viem.ts +++ b/sdk/js/src/viem.ts @@ -33,6 +33,6 @@ export function toViemAccountSecure(keyResponse: GetKeyResponse | GetTlsKeyRespo const hex = bytesToHex(sha256(keyResponse.asUint8Array())) return privateKeyToAccount(`0x${hex}`) } - const hex = Array.from(keyResponse.key).map(b => b.toString(16).padStart(2, '0')).join('') + const hex = bytesToHex(sha256(keyResponse.key)) return privateKeyToAccount(`0x${hex}`) } diff --git a/sdk/python/src/dstack_sdk/ethereum.py b/sdk/python/src/dstack_sdk/ethereum.py index 9475f8370..cc2982040 100644 --- a/sdk/python/src/dstack_sdk/ethereum.py +++ b/sdk/python/src/dstack_sdk/ethereum.py @@ -63,4 +63,5 @@ def to_account_secure( "to_account_secure: missing SHA256 support, please upgrade your system" ) from e else: # GetKeyResponse - return Account.from_key(get_key_response.decode_key()) # type: ignore[no-any-return] + hashed_key = hashlib.sha256(get_key_response.decode_key()).digest() + return Account.from_key(hashed_key) # type: ignore[no-any-return] diff --git a/sdk/python/src/dstack_sdk/solana.py b/sdk/python/src/dstack_sdk/solana.py index ba8c1b153..6f4ad4189 100644 --- a/sdk/python/src/dstack_sdk/solana.py +++ b/sdk/python/src/dstack_sdk/solana.py @@ -61,4 +61,5 @@ def to_keypair_secure(get_key_response: GetKeyResponse | GetTlsKeyResponse) -> K "to_keypair_secure: missing SHA256 support, please upgrade your system" ) from e else: # GetKeyResponse - return Keypair.from_seed(get_key_response.decode_key()) + hashed_key = hashlib.sha256(get_key_response.decode_key()).digest() + return Keypair.from_seed(hashed_key) diff --git a/sdk/python/tests/test_ethereum.py b/sdk/python/tests/test_ethereum.py index c270ae5bc..a9eccae51 100644 --- a/sdk/python/tests/test_ethereum.py +++ b/sdk/python/tests/test_ethereum.py @@ -104,3 +104,17 @@ def test_to_account_secure_with_tls_key(): assert len(w) == 1 assert issubclass(w[0].category, DeprecationWarning) assert "Please don't use getTlsKey method" in str(w[0].message) + + +def test_to_account_secure_hashes_full_key_material_for_get_key_response(): + """Regression: the GetKeyResponse branch previously fell through unhashed, + silently contradicting the "SHA256 of full key material" docstring.""" + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + + legacy_account = to_account(mock_result) + secure_account = to_account_secure(mock_result) + + assert secure_account.address != legacy_account.address diff --git a/sdk/python/tests/test_solana.py b/sdk/python/tests/test_solana.py index b263184fd..6f2c6abc6 100644 --- a/sdk/python/tests/test_solana.py +++ b/sdk/python/tests/test_solana.py @@ -76,3 +76,17 @@ def test_to_keypair_secure_with_tls_key(): assert len(w) == 1 assert issubclass(w[0].category, DeprecationWarning) assert "Please don't use getTlsKey method" in str(w[0].message) + + +def test_to_keypair_secure_hashes_full_key_material_for_get_key_response(): + """Regression: the GetKeyResponse branch previously fell through unhashed, + silently contradicting the "SHA256 of full key material" docstring.""" + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + + legacy_keypair = to_keypair(mock_result) + secure_keypair = to_keypair_secure(mock_result) + + assert bytes(secure_keypair.pubkey()) != bytes(legacy_keypair.pubkey())