Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- http-client: a caller can bound the response body (`http_request_bounded`, `PrpcClient::with_max_response_bytes`). Nothing is bounded by default — `dstack vmm logs --lines 100000` is a legitimate multi-megabyte fetch — but every client that talks to a guest agent opts in, in the gateway and in the VMM, because a CVM is untrusted and one of them polls on a timer against the whole fleet

### Fixed
- sdk: Python and JavaScript blockchain adapters now reject TLS-key responses. The deprecated conversion path read fixed PKCS#8 framing as private-key bytes, causing different TLS keys to derive the same Ethereum and Solana wallets. Use `get_key()` / `getKey()` for wallet keys.
- data disks: discard now propagates through ZFS or ext4, dm-crypt, virtio-blk, and QEMU so encrypted qcow2 images release deleted blocks instead of growing with lifetime writes. Discard defaults on and can be disabled with `storage_discard: false` when allocation-pattern leakage is unacceptable; upgrading an existing ZFS pool also starts a one-time trim for historical free space
- certbot: a certificate covering both a name and its wildcard (`example.com` and `*.example.com`) could never be issued over dns-01. The two authorizations are answered under one `_acme-challenge.example.com`, each with its own TXT value, and the publish step cleared every TXT record at that name before writing its own -- so the second authorization deleted the record answering the first, and the order failed with `Correct value not found for DNS challenge`. Clearing leftovers from an aborted run is now done once per challenge name per issuance, and the records for one name accumulate instead of replacing each other; cleanup afterwards is unchanged, deleting each record this run created by id
- certbot: editing `domains` in `certbot.toml` had no effect once a certificate existed. Issuance was skipped whenever `live/cert.pem` was present, whatever names it carried, and renewal read its name list back off that certificate rather than the configuration -- so an added or removed name never reached the CA, and the mismatch survived every renewal. The live certificate's DNS names are now compared against the configured list (as sets, case- and trailing-dot-insensitive) and a mismatch reissues, logging both lists. A reissue that fails does not take the renewal check down with it: a name the CA will not validate is reported on every cycle, while the certificate actually being served keeps renewing, and the failure is still what the run returns unless the renewal committed something of its own
Expand Down
102 changes: 28 additions & 74 deletions sdk/js/src/__tests__/solana.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,37 @@
//
// SPDX-License-Identifier: Apache-2.0


import { expect, describe, it, vi } from 'vitest'
import { expect, describe, it } from 'vitest'
import { Keypair } from '@solana/web3.js'

import { DstackClientV0, TappdClient } from '../index'
import type { GetKeyResponse, GetTlsKeyResponse } from '../client-v0'
import { toKeypair, toKeypairSecure } from '../solana'

describe('solana support', () => {
describe('toKeypair (legacy)', () => {
it('should able to get keypair from getKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const result = await client.getKey('/', 'test')
const keypair = toKeypair(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
})

it('should able to get keypair from deriveKey with TappdClient', async () => {
const client = new TappdClient()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.deriveKey('/', 'test')
const keypair = toKeypair(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
expect(consoleSpy).toHaveBeenCalledWith('toKeypair: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})

it('should able to get keypair from getTlsKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.getTlsKey()
const keypair = toKeypair(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
expect(consoleSpy).toHaveBeenCalledWith('toKeypair: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})
})

describe('toKeypairSecure', () => {
it('should able to get keypair from getKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const result = await client.getKey('/', 'test')
const keypair = toKeypairSecure(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
})

it('should able to get keypair from deriveKey with TappdClient', async () => {
const client = new TappdClient()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.deriveKey('/', 'test')
const keypair = toKeypairSecure(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
expect(consoleSpy).toHaveBeenCalledWith('toKeypairSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})

it('should able to get keypair from getTlsKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.getTlsKey()
const keypair = toKeypairSecure(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
expect(consoleSpy).toHaveBeenCalledWith('toKeypairSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
for (const [name, adapter] of [
['toKeypair', toKeypair],
['toKeypairSecure', toKeypairSecure],
] as const) {
describe(name, () => {
it('creates a keypair from getKey', async () => {
const result: GetKeyResponse = {
__name__: 'GetKeyResponse',
key: new Uint8Array(32).fill(1),
signature_chain: [],
}
const keypair = adapter(result)
expect(keypair).toBeInstanceOf(Keypair)
expect(keypair.secretKey.length).toBe(64)
})

it('rejects TLS keys', async () => {
const result: GetTlsKeyResponse = {
__name__: 'GetTlsKeyResponse',
key: 'not used',
certificate_chain: [],
asUint8Array: () => new Uint8Array(),
}
expect(() => adapter(result as never)).toThrow(/TLS keys cannot be used/)
})
})
})
}
})
113 changes: 28 additions & 85 deletions sdk/js/src/__tests__/viem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,93 +2,36 @@
//
// SPDX-License-Identifier: Apache-2.0


import { expect, describe, it, vi } from 'vitest'
import { DstackClientV0, TappdClient } from '../index'
import { expect, describe, it } from 'vitest'
import type { GetKeyResponse, GetTlsKeyResponse } from '../client-v0'
import { toViemAccount, toViemAccountSecure } from '../viem'

describe('viem support', () => {
describe('toViemAccount (legacy)', () => {
it('should able to get account from getKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const result = await client.getKey('/', 'test')
const account = toViemAccount(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
})

it('should able to get account from deriveKey with TappdClient', async () => {
const client = new TappdClient()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.deriveKey('/', 'test')
const account = toViemAccount(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
expect(consoleSpy).toHaveBeenCalledWith('toViemAccount: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})

it('should able to get account from getTlsKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.getTlsKey()
const account = toViemAccount(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
expect(consoleSpy).toHaveBeenCalledWith('toViemAccount: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})
})

describe('toViemAccountSecure', () => {
it('should able to get account from getKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const result = await client.getKey('/', 'test')
const account = toViemAccountSecure(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
})

it('should able to get account from deriveKey with TappdClient', async () => {
const client = new TappdClient()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.deriveKey('/', 'test')
const account = toViemAccountSecure(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
expect(consoleSpy).toHaveBeenCalledWith('toViemAccountSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
})

it('should able to get account from getTlsKey with DstackClientV0', async () => {
const client = new DstackClientV0()
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})

const result = await client.getTlsKey()
const account = toViemAccountSecure(result)

expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
expect(typeof account.signMessage).toBe('function')
expect(consoleSpy).toHaveBeenCalledWith('toViemAccountSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')

consoleSpy.mockRestore()
for (const [name, adapter] of [
['toViemAccount', toViemAccount],
['toViemAccountSecure', toViemAccountSecure],
] as const) {
describe(name, () => {
it('creates an account from getKey', async () => {
const result: GetKeyResponse = {
__name__: 'GetKeyResponse',
key: new Uint8Array(32).fill(1),
signature_chain: [],
}
const account = adapter(result)
expect(account.source).toBe('privateKey')
expect(typeof account.sign).toBe('function')
})

it('rejects TLS keys', async () => {
const result: GetTlsKeyResponse = {
__name__: 'GetTlsKeyResponse',
key: 'not used',
certificate_chain: [],
asUint8Array: () => new Uint8Array(),
}
expect(() => adapter(result as never)).toThrow(/TLS keys cannot be used/)
})
})
})
}
})
34 changes: 12 additions & 22 deletions sdk/js/src/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,25 @@
//
// SPDX-License-Identifier: Apache-2.0

import { sha256 } from '@noble/hashes/sha256'
import { type GetKeyResponse, type GetTlsKeyResponse } from './client-v0'
import { type GetKeyResponse } from './client-v0'
import { Keypair } from '@solana/web3.js'

/**
* @deprecated use toKeypairSecure instead. This method has security concerns.
* Current implementation uses raw key material without proper hashing.
*/
export function toKeypair(keyResponse: GetTlsKeyResponse | GetKeyResponse) {
// Keep legacy behavior for GetTlsKeyResponse, but with warning.
function rejectTlsKey(keyResponse: { readonly __name__: string }): void {
if (keyResponse.__name__ === 'GetTlsKeyResponse') {
console.warn('toKeypair: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')
// Restored original behavior: using first 32 bytes directly
const bytes = keyResponse.asUint8Array(32)
return Keypair.fromSeed(bytes)
throw new TypeError('TLS keys cannot be used to derive Solana keypairs; use getKey()')
}
return Keypair.fromSeed(keyResponse.key)
}

/**
* Creates a Solana Keypair from DeriveKeyResponse using secure key derivation.
* This method applies SHA256 hashing to the complete key material for enhanced security.
* @deprecated use toKeypairSecure instead.
*/
export function toKeypairSecure(keyResponse: GetTlsKeyResponse | GetKeyResponse) {
// Keep legacy behavior for GetTlsKeyResponse, but with warning.
if (keyResponse.__name__ === 'GetTlsKeyResponse') {
console.warn('toKeypairSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')
const buf = sha256(keyResponse.asUint8Array())
return Keypair.fromSeed(buf)
}
export function toKeypair(keyResponse: GetKeyResponse) {
rejectTlsKey(keyResponse)
return Keypair.fromSeed(keyResponse.key)
}

/** Creates a Solana keypair from a getKey() response. */
export function toKeypairSecure(keyResponse: GetKeyResponse) {
rejectTlsKey(keyResponse)
return Keypair.fromSeed(keyResponse.key)
}
36 changes: 13 additions & 23 deletions sdk/js/src/viem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,27 @@
//
// SPDX-License-Identifier: Apache-2.0

import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex } from '@noble/hashes/utils'
import { type GetKeyResponse, type GetTlsKeyResponse } from './client-v0'
import { type GetKeyResponse } from './client-v0'
import { privateKeyToAccount } from 'viem/accounts'

/**
* @deprecated use toViemAccountSecure instead. This method has security concerns.
* Current implementation uses raw key material without proper hashing.
*/
export function toViemAccount(keyResponse: GetKeyResponse | GetTlsKeyResponse) {
// Keep legacy behavior for GetTlsKeyResponse, but with warning.
function rejectTlsKey(keyResponse: { readonly __name__: string }): void {
if (keyResponse.__name__ === 'GetTlsKeyResponse') {
console.warn('toViemAccount: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')
const hex = Array.from(keyResponse.asUint8Array(32)).map(b => b.toString(16).padStart(2, '0')).join('')
return privateKeyToAccount(`0x${hex}`)
throw new TypeError('TLS keys cannot be used to derive Viem accounts; use getKey()')
}
const hex = Array.from(keyResponse.key).map(b => b.toString(16).padStart(2, '0')).join('')
return privateKeyToAccount(`0x${hex}`)
}

/**
* Creates a Viem account from DeriveKeyResponse using secure key derivation.
* This method applies SHA256 hashing to the complete key material for enhanced security.
* @deprecated use toViemAccountSecure instead.
*/
export function toViemAccountSecure(keyResponse: GetKeyResponse | GetTlsKeyResponse) {
// Keep legacy behavior for GetTlsKeyResponse, but with warning.
if (keyResponse.__name__ === 'GetTlsKeyResponse') {
console.warn('toViemAccountSecure: Please don\'t use `deriveKey` method to get key, use `getKey` instead.')
const hex = bytesToHex(sha256(keyResponse.asUint8Array()))
return privateKeyToAccount(`0x${hex}`)
}
export function toViemAccount(keyResponse: GetKeyResponse) {
rejectTlsKey(keyResponse)
const hex = Array.from(keyResponse.key).map(b => b.toString(16).padStart(2, '0')).join('')
return privateKeyToAccount(`0x${hex}`)
}

/** Creates a Viem account from a getKey() response. */
export function toViemAccountSecure(keyResponse: GetKeyResponse) {
rejectTlsKey(keyResponse)
const hex = Array.from(keyResponse.key).map(b => b.toString(16).padStart(2, '0')).join('')
return privateKeyToAccount(`0x${hex}`)
}
Loading
Loading