Skip to content

Commit cb8a8e1

Browse files
authored
Merge pull request #695 from Dstack-TEE/feat/sdk-python-api-parity
feat(sdk/python): API parity with sdk/rust + guest-agent (0.5.4b1)
2 parents 67fad0c + 240326e commit cb8a8e1

8 files changed

Lines changed: 546 additions & 1106 deletions

File tree

sdk/python/README.md

Lines changed: 152 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ Access TEE features from your Python application running inside dstack. Derive d
88
pip install dstack-sdk
99
```
1010

11+
Blockchain helpers are optional extras:
12+
13+
| Extra | Pulls in | Use when |
14+
|---|---|---|
15+
| `dstack-sdk[ethereum]` | `eth-account` | You want `to_account` / `to_account_secure` for Ethereum signing |
16+
| `dstack-sdk[solana]` | `solders` | You want `to_keypair` / `to_keypair_secure` for Solana signing |
17+
| `dstack-sdk[all]` | both | You need both |
18+
19+
Aliases `[eth]` and `[sol]` are accepted for convenience.
20+
1121
## Quick Start
1222

1323
```python
@@ -24,10 +34,11 @@ quote = client.get_quote(b'my-app-state')
2434
print(quote.quote)
2535
```
2636

27-
The client automatically connects to `/var/run/dstack.sock`. For local development with the simulator, pass the endpoint explicitly:
37+
The client automatically connects to `/var/run/dstack.sock`. For local development with the simulator:
2838

2939
```python
3040
client = DstackClient('http://localhost:8090')
41+
# or export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090
3142
```
3243

3344
## Core API
@@ -45,18 +56,19 @@ btc_key = client.get_key('wallet/bitcoin')
4556
mainnet_key = client.get_key('wallet/eth/mainnet')
4657
testnet_key = client.get_key('wallet/eth/testnet')
4758

48-
# Different algorithm
59+
# Use a different signature algorithm (requires dstack OS >= 0.5.7)
4960
ed_key = client.get_key('signing/key', algorithm='ed25519')
5061
```
5162

5263
**Parameters:**
53-
- `path`: Key derivation path (determines the key)
54-
- `purpose` (optional): Included in signature chain message, does not affect the derived key
55-
- `algorithm` (optional): `'secp256k1'` (default) or `'ed25519'`
64+
- `path` (optional): Key derivation path. Defaults to `""` (root).
65+
- `purpose` (optional): Included in the signature chain message; does not affect the derived key.
66+
- `algorithm` (optional): `'secp256k1'` (default) or `'ed25519'`.
5667

5768
**Returns:** `GetKeyResponse`
5869
- `key`: Hex-encoded private key
5970
- `signature_chain`: Signatures proving the key was derived in a genuine TEE
71+
- `decode_key()` / `decode_signature_chain()`: Helpers that return `bytes`
6072

6173
### Generate Attestation Quotes
6274

@@ -71,12 +83,23 @@ print(rtmrs)
7183
```
7284

7385
**Parameters:**
74-
- `report_data`: Exactly 64 bytes recommended. If shorter, pad with zeros. If longer, hash it first (e.g., SHA-256).
86+
- `report_data`: Up to 64 bytes (`bytes` or `str`). Shorter inputs are padded with zeros; longer inputs should be hashed first (e.g., SHA-256).
7587

7688
**Returns:** `GetQuoteResponse`
7789
- `quote`: Hex-encoded TDX quote
7890
- `event_log`: JSON string of measured events
79-
- `replay_rtmrs()`: Method to compute RTMR values from event log
91+
- `replay_rtmrs()`: Method to compute RTMR values from the event log
92+
- `decode_quote()` / `decode_event_log()`: Helpers
93+
94+
### Versioned Attestation
95+
96+
`attest()` returns a versioned attestation payload that newer verifier APIs can dispatch on without sniffing the quote format.
97+
98+
```python
99+
result = client.attest(b'user:alice:nonce123')
100+
print(result.attestation) # hex string
101+
print(result.decode_attestation()) # bytes
102+
```
80103

81104
### Get Instance Info
82105

@@ -85,15 +108,16 @@ info = client.info()
85108
print(info.app_id)
86109
print(info.instance_id)
87110
print(info.tcb_info)
111+
print(info.cloud_vendor, info.cloud_product) # 0.5.7+
88112
```
89113

90114
**Returns:** `InfoResponse`
91-
- `app_id`: Application identifier
92-
- `instance_id`: Instance identifier
93-
- `app_name`: Application name
94-
- `tcb_info`: TCB measurements (MRTD, RTMRs, event log)
115+
- `app_id`, `instance_id`, `app_name`, `device_id`
116+
- `tcb_info`: TCB measurements (MRTD, RTMRs, event log, compose hash, ...)
95117
- `compose_hash`: Hash of the app configuration
96118
- `app_cert`: Application certificate (PEM)
119+
- `key_provider_info`: Key management configuration
120+
- `cloud_vendor` / `cloud_product`: Cloud provider strings (empty on older OS)
97121

98122
### Generate TLS Certificates
99123

@@ -103,27 +127,35 @@ print(info.tcb_info)
103127
tls = client.get_tls_key(
104128
subject='api.example.com',
105129
alt_names=['localhost'],
106-
usage_ra_tls=True # Embed attestation in certificate
130+
usage_ra_tls=True, # Embed attestation in certificate
131+
# 0.5.7+ options below:
132+
not_before=1700000000, # seconds since UNIX epoch
133+
not_after=1800000000,
134+
with_app_info=True,
107135
)
108-
109-
print(tls.key) # PEM private key
110-
print(tls.certificate_chain) # Certificate chain
136+
print(tls.key) # PEM private key
137+
print(tls.certificate_chain) # Certificate chain
111138
```
112139

113140
**Parameters:**
114-
- `subject` (optional): Certificate common name (e.g., domain name)
115-
- `alt_names` (optional): List of subject alternative names
116-
- `usage_ra_tls` (optional): Embed TDX quote in certificate extension
117-
- `usage_server_auth` (optional): Enable for server authentication (default: `True`)
118-
- `usage_client_auth` (optional): Enable for client authentication (default: `False`)
141+
- `subject` (optional): Certificate Common Name (e.g., domain name)
142+
- `alt_names` (optional): Subject Alternative Names
143+
- `usage_ra_tls` (optional): Embed TDX quote in a certificate extension (default `False`)
144+
- `usage_server_auth` (optional): Enable for server authentication (default `True`)
145+
- `usage_client_auth` (optional): Enable for client authentication (default `False`)
146+
- `not_before` / `not_after` (optional, kw-only): Validity window in seconds since UNIX epoch. Requires dstack OS >= 0.5.7.
147+
- `with_app_info` (optional, kw-only): Embed app identity into the certificate. Requires dstack OS >= 0.5.7.
148+
149+
When any of the 0.5.7-only options is set, the SDK probes `Version` first and raises `RuntimeError` on older guest agents that lack it.
119150

120151
**Returns:** `GetTlsKeyResponse`
121152
- `key`: PEM-encoded private key
122153
- `certificate_chain`: List of PEM certificates
154+
- `as_uint8array(max_length=None)`: Returns the DER-encoded private key bytes (handy when feeding key material into low-level crypto libraries)
123155

124156
### Sign and Verify
125157

126-
Sign data using TEE-derived keys (not yet released):
158+
Sign data using TEE-derived keys:
127159

128160
```python
129161
result = client.sign('ed25519', b'message to sign')
@@ -137,21 +169,15 @@ print(valid.valid) # True
137169

138170
**`sign()` Parameters:**
139171
- `algorithm`: `'ed25519'`, `'secp256k1'`, or `'secp256k1_prehashed'`
140-
- `data`: Data to sign (bytes or string)
172+
- `data`: Data to sign (`bytes` or `str`). For `secp256k1_prehashed`, must be a 32-byte digest.
141173

142174
**`sign()` Returns:** `SignResponse`
143175
- `signature`: Hex-encoded signature
144176
- `public_key`: Hex-encoded public key
145177
- `signature_chain`: Signatures proving TEE origin
146178

147-
**`verify()` Parameters:**
148-
- `algorithm`: Algorithm used for signing
149-
- `data`: Original data
150-
- `signature`: Signature to verify
151-
- `public_key`: Public key to verify against
152-
153179
**`verify()` Returns:** `VerifyResponse`
154-
- `valid`: Boolean indicating if signature is valid
180+
- `valid`: Boolean indicating if the signature is valid
155181

156182
### Emit Events
157183

@@ -162,25 +188,28 @@ client.emit_event('config_loaded', 'production')
162188
client.emit_event('plugin_initialized', 'auth-v2')
163189
```
164190

165-
**Parameters:**
166-
- `event`: Event name (string identifier)
167-
- `payload`: Event value (bytes or string)
191+
### Diagnostics
192+
193+
```python
194+
client.version() # VersionResponse(version, rev) — raises on OS < 0.5.7
195+
client.is_reachable() # Quick connectivity probe; never raises
196+
```
168197

169198
## Async Client
170199

171-
For async applications, use `AsyncDstackClient`:
200+
For async applications, use `AsyncDstackClient`. The API surface is identical, but every method is a coroutine:
172201

173202
```python
174-
from dstack_sdk import AsyncDstackClient
175203
import asyncio
204+
from dstack_sdk import AsyncDstackClient
176205

177206
async def main():
178207
client = AsyncDstackClient()
179208

180209
info = await client.info()
181210
key = await client.get_key('wallet/eth')
182211

183-
# Concurrent operations
212+
# Run requests concurrently
184213
keys = await asyncio.gather(
185214
client.get_key('user/alice'),
186215
client.get_key('user/bob'),
@@ -189,79 +218,91 @@ async def main():
189218
asyncio.run(main())
190219
```
191220

221+
`AsyncDstackClient` accepts the same constructor as `DstackClient` plus `use_sync_http: bool = False` for callers that need to issue sync HTTP from within an async context.
222+
192223
## Blockchain Integration
193224

194225
### Ethereum
195226

196227
```python
197-
from dstack_sdk.ethereum import to_account
228+
from dstack_sdk.ethereum import to_account_secure
198229

199230
key = client.get_key('wallet/ethereum')
200-
account = to_account(key)
231+
account = to_account_secure(key)
201232
print(account.address)
202233
```
203234

235+
`to_account_secure(key)` hashes the full key material with SHA-256 before deriving the Ethereum private key. The legacy `to_account()` is kept for backward compatibility but uses raw key bytes—prefer the secure variant for new code.
236+
204237
### Solana
205238

206239
```python
207-
from dstack_sdk.solana import to_keypair
240+
from dstack_sdk.solana import to_keypair_secure
208241

209242
key = client.get_key('wallet/solana')
210-
keypair = to_keypair(key)
211-
print(keypair.public_key)
212-
```
213-
214-
## Development
215-
216-
For local development without TDX hardware, use the simulator:
217-
218-
```bash
219-
git clone https://github.com/Dstack-TEE/dstack.git
220-
cd dstack/sdk/simulator
221-
./build.sh
222-
./dstack-simulator
223-
```
224-
225-
Then set the endpoint:
226-
227-
```bash
228-
export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090
243+
keypair = to_keypair_secure(key)
244+
print(keypair.pubkey())
229245
```
230246

231-
Run tests with PDM:
232-
233-
```bash
234-
pdm install -d
235-
pdm run pytest -s
236-
```
247+
Same pattern: `to_keypair_secure(key)` SHA-256-hashes the key material; `to_keypair()` is the legacy raw-bytes variant.
237248

238249
---
239250

240251
## Deployment Utilities
241252

242253
These utilities are for deployment scripts, not runtime SDK operations.
243254

244-
### Encrypt Environment Variables
255+
### Encrypted Environment Variables
245256

246-
Encrypt secrets before deploying to dstack:
257+
The KMS returns a fresh X25519 public key (with a secp256k1 signature) that you encrypt secrets against before submitting them with your deployment. Always verify the signer before trusting the key:
247258

248259
```python
249-
from dstack_sdk import encrypt_env_vars, verify_env_encrypt_public_key, EnvVar
260+
from dstack_sdk import (
261+
encrypt_env_vars,
262+
verify_env_encrypt_public_key,
263+
verify_env_encrypt_public_key_legacy,
264+
EnvVar,
265+
)
250266

251-
# Get and verify the KMS public key
252-
# (obtain public_key and signature from KMS API)
253-
kms_identity = verify_env_encrypt_public_key(public_key_bytes, signature_bytes, app_id)
254-
if not kms_identity:
255-
raise RuntimeError('Invalid KMS key')
267+
# `public_key`, `signature_v1`, `timestamp` come from KMS /GetAppEnvEncryptPubKey.
268+
signer = verify_env_encrypt_public_key(
269+
public_key=public_key_bytes,
270+
signature=signature_v1_bytes,
271+
app_id=app_id_hex,
272+
timestamp=timestamp,
273+
)
274+
if signer is None:
275+
# Fallback for older KMS builds that only emit the unprotected legacy
276+
# signature. Vulnerable to replay; warn loudly if you must use it.
277+
signer = verify_env_encrypt_public_key_legacy(
278+
public_key=public_key_bytes,
279+
signature=legacy_signature_bytes,
280+
app_id=app_id_hex,
281+
)
282+
if signer is None:
283+
raise RuntimeError('invalid KMS env-encrypt public key')
284+
285+
# Always compare the recovered signer against a known-good KMS signer
286+
# address, obtained out-of-band from the DstackKms contract or your
287+
# deployment configuration. Without this check, an attacker could sign
288+
# their own env-encrypt key and the verification above would still pass.
289+
EXPECTED_KMS_SIGNER = '0x...' # replace with your known KMS signer address
290+
if signer != EXPECTED_KMS_SIGNER:
291+
raise RuntimeError(
292+
f'unexpected KMS signer: got {signer}, '
293+
f'expected {EXPECTED_KMS_SIGNER}'
294+
)
256295

257-
# Encrypt variables
258296
env_vars = [
259297
EnvVar(key='DATABASE_URL', value='postgresql://...'),
260298
EnvVar(key='API_KEY', value='secret'),
261299
]
262-
encrypted = encrypt_env_vars(env_vars, public_key)
300+
encrypted = await encrypt_env_vars(env_vars, public_key_hex)
301+
# encrypt_env_vars_sync(...) is also available for non-async callers.
263302
```
264303

304+
`verify_env_encrypt_public_key` returns the recovered compressed secp256k1 signer (`0x`-prefixed hex) on success, or `None` for any failure (bad length, expired/future timestamp, malformed `app_id`, invalid signature). The default `max_age_seconds` is 300; pass a larger value if your deployment workflow legitimately holds the response longer.
305+
265306
### Calculate Compose Hash
266307

267308
```python
@@ -272,6 +313,43 @@ hash_value = get_compose_hash(app_compose_dict)
272313

273314
---
274315

316+
## Compatibility
317+
318+
| Feature | Required dstack OS |
319+
|---|---|
320+
| `get_key`, `get_quote`, `get_tls_key` (legacy fields), `info` (legacy fields) | 0.3+ |
321+
| `emit_event` | 0.5.0+ |
322+
| `attest`, `sign` / `verify`, `is_reachable` | 0.5.0+ (sign/verify require server build with the feature) |
323+
| `version`, `algorithm='ed25519'` on `get_key`, `info.cloud_vendor` / `cloud_product`, `not_before` / `not_after` / `with_app_info` on `get_tls_key` | 0.5.7+ |
324+
| `verify_env_encrypt_public_key` (signature_v1 with timestamp) | Requires KMS build that emits `signature_v1`; legacy variant remains available |
325+
326+
Calls that require 0.5.7-only fields probe the `Version` RPC first and raise a clear `RuntimeError` on older guest agents.
327+
328+
## Development
329+
330+
For local development without TDX hardware, use the simulator:
331+
332+
```bash
333+
git clone https://github.com/Dstack-TEE/dstack.git
334+
cd dstack/sdk/simulator
335+
./build.sh
336+
./dstack-simulator
337+
```
338+
339+
Then set the endpoint:
340+
341+
```bash
342+
export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090
343+
```
344+
345+
Install dev dependencies and run tests with PDM:
346+
347+
```bash
348+
cd sdk/python
349+
make install
350+
make test
351+
```
352+
275353
## Migration from TappdClient
276354

277355
Replace `TappdClient` with `DstackClient`:
@@ -288,7 +366,7 @@ client = DstackClient()
288366

289367
Method changes:
290368
- `derive_key()``get_tls_key()` for TLS certificates
291-
- `tdx_quote()``get_quote()`
369+
- `tdx_quote()``get_quote()` (raw data only, no hash algorithms)
292370
- Socket path: `/var/run/tappd.sock``/var/run/dstack.sock`
293371

294372
## License

0 commit comments

Comments
 (0)