Skip to content

Commit 9dd0412

Browse files
committed
feat(sdk): validate algorithm in getKey by checking OS version
When getKey is called with a non-secp256k1 algorithm (e.g. ed25519), the SDK now calls the Version RPC to check if the OS supports it. If the Version RPC is unavailable (OS <= 0.5.6), the call is rejected with a clear error instead of silently returning the wrong key type. The version check result is cached for the lifetime of the client.
1 parent c6b2d94 commit 9dd0412

7 files changed

Lines changed: 115 additions & 5 deletions

File tree

sdk/go/dstack/client.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"net/http"
2020
"os"
2121
"strings"
22+
"sync"
2223
)
2324

2425
// Represents the response from a TLS key derivation request.
@@ -174,6 +175,9 @@ type DstackClient struct {
174175
baseURL string
175176
httpClient *http.Client
176177
logger *slog.Logger
178+
179+
versionOnce sync.Once
180+
versionSupported bool
177181
}
178182

179183
// Functional option for configuring a DstackClient.
@@ -369,8 +373,37 @@ func (c *DstackClient) GetTlsKey(
369373
return &response, nil
370374
}
371375

376+
// requiresVersionCheck returns true for algorithms that need OS >= 0.5.7.
377+
func requiresVersionCheck(algorithm string) bool {
378+
switch algorithm {
379+
case "secp256k1", "secp256k1_prehashed", "k256", "":
380+
return false
381+
default:
382+
return true
383+
}
384+
}
385+
386+
// ensureAlgorithmSupported checks the OS version when a non-secp256k1 algorithm is requested.
387+
// On old OS (no Version RPC), it returns an error to prevent silent key type mismatch.
388+
func (c *DstackClient) ensureAlgorithmSupported(ctx context.Context, algorithm string) error {
389+
if !requiresVersionCheck(algorithm) {
390+
return nil
391+
}
392+
c.versionOnce.Do(func() {
393+
_, err := c.GetVersion(ctx)
394+
c.versionSupported = err == nil
395+
})
396+
if !c.versionSupported {
397+
return fmt.Errorf("algorithm %q is not supported: OS version too old (Version RPC unavailable)", algorithm)
398+
}
399+
return nil
400+
}
401+
372402
// Gets a key from the dstack service.
373403
func (c *DstackClient) GetKey(ctx context.Context, path string, purpose string, algorithm string) (*GetKeyResponse, error) {
404+
if err := c.ensureAlgorithmSupported(ctx, algorithm); err != nil {
405+
return nil, err
406+
}
374407
payload := map[string]interface{}{
375408
"path": path,
376409
"purpose": purpose,

sdk/js/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,11 @@ export interface TlsKeyOptions {
177177
usageClientAuth?: boolean;
178178
}
179179

180+
const SECP256K1_ALGORITHMS = new Set(['secp256k1', 'secp256k1_prehashed', 'k256', ''])
181+
180182
export class DstackClient<T extends TcbInfo = TcbInfoV05x> {
181183
protected endpoint: string
184+
private _versionChecked: boolean | undefined = undefined
182185

183186
constructor(endpoint: string | undefined = undefined) {
184187
if (endpoint === undefined) {
@@ -202,7 +205,23 @@ export class DstackClient<T extends TcbInfo = TcbInfoV05x> {
202205
this.endpoint = endpoint
203206
}
204207

208+
private async ensureAlgorithmSupported(algorithm: string): Promise<void> {
209+
if (SECP256K1_ALGORITHMS.has(algorithm)) return
210+
if (this._versionChecked === undefined) {
211+
try {
212+
await this.version()
213+
this._versionChecked = true
214+
} catch {
215+
this._versionChecked = false
216+
}
217+
}
218+
if (!this._versionChecked) {
219+
throw new Error(`algorithm "${algorithm}" is not supported: OS version too old (Version RPC unavailable)`)
220+
}
221+
}
222+
205223
async getKey(path: string, purpose: string = '', algorithm: string = 'secp256k1'): Promise<GetKeyResponse> {
224+
await this.ensureAlgorithmSupported(algorithm)
206225
const payload = JSON.stringify({
207226
path: path,
208227
purpose: purpose,

sdk/python/src/dstack_sdk/dstack_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ def __init__(
303303
self._sync_client: Optional[httpx.Client] = None
304304
self._client_ref_count = 0
305305
self._timeout = timeout
306+
self._version_checked: Optional[bool] = None
306307

307308
if endpoint.startswith("http://") or endpoint.startswith("https://"):
308309
self.async_transport = httpx.AsyncHTTPTransport()
@@ -380,13 +381,30 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
380381
self._sync_client.close()
381382
self._sync_client = None
382383

384+
async def _ensure_algorithm_supported(self, algorithm: str) -> None:
385+
"""Check OS version when a non-secp256k1 algorithm is requested."""
386+
if algorithm in ("secp256k1", "secp256k1_prehashed", "k256", ""):
387+
return
388+
if self._version_checked is None:
389+
try:
390+
await self.version()
391+
self._version_checked = True
392+
except Exception:
393+
self._version_checked = False
394+
if not self._version_checked:
395+
raise RuntimeError(
396+
f'algorithm "{algorithm}" is not supported: '
397+
"OS version too old (Version RPC unavailable)"
398+
)
399+
383400
async def get_key(
384401
self,
385402
path: str | None = None,
386403
purpose: str | None = None,
387404
algorithm: str = "secp256k1",
388405
) -> GetKeyResponse:
389406
"""Derive a key from the given path, purpose, and algorithm."""
407+
await self._ensure_algorithm_supported(algorithm)
390408
data: Dict[str, Any] = {
391409
"path": path or "",
392410
"purpose": purpose or "",

sdk/rust/examples/dstack_client_usage.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ async fn main() -> anyhow::Result<()> {
3434

3535
// 2. Derive a key
3636
let response = client
37-
.get_key(Some("my-app".to_string()), Some("encryption".to_string()))
37+
.get_key(Some("my-app".to_string()), Some("encryption".to_string()), None)
3838
.await?;
3939
println!("Key derived successfully!");
4040
println!(" Key length: {}", response.key.len());
@@ -93,7 +93,7 @@ async fn main() -> anyhow::Result<()> {
9393
);
9494

9595
// 6. Get a simple key without purpose
96-
let response = client.get_key(Some("simple-key".to_string()), None).await?;
96+
let response = client.get_key(Some("simple-key".to_string()), None, None).await?;
9797
println!("Simple key derived successfully!");
9898
println!(" Key: {}", response.key);
9999

sdk/rust/src/dstack_client.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use reqwest::Client;
1111
use serde::{de::DeserializeOwned, Serialize};
1212
use serde_json::{json, Value};
1313
use std::env;
14+
use std::sync::Mutex;
1415

1516
pub use dstack_sdk_types::dstack::*;
1617

@@ -60,6 +61,14 @@ pub enum ClientKind {
6061

6162
pub trait BaseClient {}
6263

64+
/// Algorithms that require OS >= 0.5.7 (i.e., Version RPC support).
65+
fn requires_version_check(algorithm: &str) -> bool {
66+
!matches!(
67+
algorithm,
68+
"secp256k1" | "secp256k1_prehashed" | "k256" | ""
69+
)
70+
}
71+
6372
/// The main client for interacting with the dstack service
6473
pub struct DstackClient {
6574
/// The base URL for HTTP requests
@@ -68,6 +77,8 @@ pub struct DstackClient {
6877
endpoint: String,
6978
/// The type of client (HTTP or Unix domain socket)
7079
client: ClientKind,
80+
/// Cached result of version check: None = not checked, Some(true) = new OS, Some(false) = old OS
81+
version_checked: Mutex<Option<bool>>,
7182
}
7283

7384
impl BaseClient for DstackClient {}
@@ -86,6 +97,7 @@ impl DstackClient {
8697
base_url,
8798
endpoint,
8899
client,
100+
version_checked: Mutex::new(None),
89101
}
90102
}
91103

@@ -126,15 +138,43 @@ impl DstackClient {
126138
}
127139
}
128140

141+
/// Check if the OS supports the given algorithm, querying version if needed.
142+
async fn ensure_algorithm_supported(&self, algorithm: &str) -> Result<()> {
143+
if !requires_version_check(algorithm) {
144+
return Ok(());
145+
}
146+
let already_checked = self.version_checked.lock().unwrap().clone();
147+
match already_checked {
148+
Some(true) => Ok(()),
149+
Some(false) => anyhow::bail!(
150+
"algorithm \"{algorithm}\" is not supported: OS version too old (Version RPC unavailable)"
151+
),
152+
None => {
153+
let supported = self.version().await.is_ok();
154+
*self.version_checked.lock().unwrap() = Some(supported);
155+
if supported {
156+
Ok(())
157+
} else {
158+
anyhow::bail!(
159+
"algorithm \"{algorithm}\" is not supported: OS version too old (Version RPC unavailable)"
160+
)
161+
}
162+
}
163+
}
164+
}
165+
129166
pub async fn get_key(
130167
&self,
131168
path: Option<String>,
132169
purpose: Option<String>,
170+
algorithm: Option<String>,
133171
) -> Result<GetKeyResponse> {
172+
let algorithm = algorithm.unwrap_or_else(|| "secp256k1".to_string());
173+
self.ensure_algorithm_supported(&algorithm).await?;
134174
let data = json!({
135175
"path": path.unwrap_or_default(),
136176
"purpose": purpose.unwrap_or_default(),
137-
"algorithm": "secp256k1", // Default or specify as needed
177+
"algorithm": algorithm,
138178
});
139179
let response = self.send_rpc_request("/GetKey", &data).await?;
140180
let response = serde_json::from_value::<GetKeyResponse>(response)?;

sdk/rust/tests/test_client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use sha2::{Digest, Sha256};
1212
#[tokio::test]
1313
async fn test_async_client_get_key() {
1414
let client = AsyncDstackClient::new(None);
15-
let result = client.get_key(None, None).await.unwrap();
15+
let result = client.get_key(None, None, None).await.unwrap();
1616
assert!(!result.key.is_empty());
1717
assert_eq!(result.decode_key().unwrap().len(), 32);
1818
}

sdk/rust/tests/test_eth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use dstack_sdk_types::dstack::GetKeyResponse;
1212
async fn test_async_to_keypair() {
1313
let client = DstackClient::new(None);
1414
let result = client
15-
.get_key(Some("test".to_string()), None)
15+
.get_key(Some("test".to_string()), None, None)
1616
.await
1717
.expect("get_key failed");
1818

0 commit comments

Comments
 (0)