Skip to content

Commit 31b6b0b

Browse files
authored
Merge pull request #263 from Dstack-TEE/rust-sdk-tappd
rust-sdk: Add support for legacy TappdClient
2 parents 1a5874b + a3a4a26 commit 31b6b0b

File tree

11 files changed

+850
-28
lines changed

11 files changed

+850
-28
lines changed

.github/workflows/sdk.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: SDK tests
2+
permissions:
3+
contents: read
4+
5+
on:
6+
push:
7+
branches: [ master, next, dev-* ]
8+
pull_request:
9+
branches: [ master, next, dev-* ]
10+
11+
env:
12+
CARGO_TERM_COLOR: always
13+
14+
jobs:
15+
sdk-tests:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Rust
21+
uses: dtolnay/[email protected]
22+
with:
23+
components: clippy, rustfmt
24+
25+
- name: SDK tests
26+
run: cd sdk && ./run-tests.sh

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

run-tests.sh

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ set -e
66
pushd sdk/simulator
77
./dstack-simulator &
88
SIMULATOR_PID=$!
9+
trap "kill $SIMULATOR_PID 2>/dev/null || true" EXIT
910
echo "Simulator process (PID: $SIMULATOR_PID) started."
1011
popd
1112

1213
export DSTACK_SIMULATOR_ENDPOINT=$(realpath sdk/simulator/dstack.sock)
14+
export TAPPD_SIMULATOR_ENDPOINT=$(realpath sdk/simulator/tappd.sock)
1315

1416
echo "DSTACK_SIMULATOR_ENDPOINT: $DSTACK_SIMULATOR_ENDPOINT"
17+
echo "TAPPD_SIMULATOR_ENDPOINT: $TAPPD_SIMULATOR_ENDPOINT"
1518

1619
# Run the tests
1720
cargo test
18-
19-
# Kill the simulator after tests finish
20-
kill $SIMULATOR_PID
21-
echo "Simulator process (PID: $SIMULATOR_PID) terminated."

sdk/run-tests.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ trap "kill $SIMULATOR_PID 2>/dev/null || true" EXIT
1313
popd
1414

1515
pushd rust/
16-
cargo test
16+
cargo test -- --show-output
17+
cargo run --example tappd_client_usage
18+
cargo run --example dstack_client_usage
1719
popd
1820

1921
pushd go/

sdk/rust/Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ authors = ["Encifher <[email protected]>"]
1010
alloy = { workspace = true, features = ["signers", "signer-local"] }
1111
anyhow.workspace = true
1212
bon.workspace = true
13-
hex.workspace = true
14-
http.workspace = true
13+
hex.workspace = true
14+
http.workspace = true
1515
http-client-unix-domain-socket = "0.1.1"
1616
reqwest = { workspace = true, features = ["json"] }
17-
serde.workspace = true
18-
serde_json.workspace = true
19-
sha2.workspace = true
17+
serde.workspace = true
18+
serde_json.workspace = true
19+
sha2.workspace = true
20+
x509-parser.workspace = true
2021

2122
[dev-dependencies]
2223
dcap-qvl.workspace = true

sdk/rust/README.md

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dstack Crate
22

3-
This crate provides a rust client for communicating with the dstack server, which is available inside dstack.
3+
This crate provides rust clients for communicating with both the current dstack server and the legacy tappd service, which are available inside dstack.
44

55
## Installation
66

@@ -11,8 +11,10 @@ dstack-rust = { git = "https://github.com/Dstack-TEE/dstack.git", package = "dst
1111

1212
## Basic Usage
1313

14+
### DstackClient (Current API)
15+
1416
```rust
15-
use dstack_sdk::DstackClient;
17+
use dstack_sdk::dstack_client::DstackClient;
1618

1719
#[tokio::main]
1820
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -40,46 +42,104 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
4042
}
4143
```
4244

45+
### TappdClient (Legacy API)
46+
47+
```rust
48+
use dstack_sdk::tappd_client::TappdClient;
49+
50+
#[tokio::main]
51+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
52+
let client = TappdClient::new(None); // Uses env var or default to Unix socket
53+
54+
// Get system info
55+
let info = client.info().await?;
56+
println!("Instance ID: {}", info.instance_id);
57+
println!("App Name: {}", info.app_name);
58+
59+
// Derive a key
60+
let key_resp = client.derive_key("my-app").await?;
61+
println!("Key: {}", key_resp.key);
62+
println!("Certificate Chain: {:?}", key_resp.certificate_chain);
63+
64+
// Decode the key to bytes (extracts raw ECDSA P-256 private key - 32 bytes)
65+
let key_bytes = key_resp.to_bytes()?;
66+
println!("ECDSA P-256 private key bytes (32 bytes): {:?}", key_bytes.len());
67+
68+
// Generate quote (exactly 64 bytes of report data required)
69+
let mut report_data = b"test-data".to_vec();
70+
report_data.resize(64, 0); // Pad to 64 bytes
71+
let quote_resp = client.get_quote(report_data).await?;
72+
println!("Quote: {}", quote_resp.quote);
73+
let rtmrs = quote_resp.replay_rtmrs()?;
74+
println!("Replayed RTMRs: {:?}", rtmrs);
75+
76+
Ok(())
77+
}
78+
```
79+
4380
## Features
44-
### Initialization
81+
82+
### DstackClient Initialization
4583

4684
```rust
4785
let client = DstackClient::new(Some("http://localhost:8000"));
4886
```
4987
- `endpoint`: Optional HTTP URL or Unix socket path (`/var/run/dstack.sock` by default)
50-
5188
- Will use the `DSTACK_SIMULATOR_ENDPOINT` environment variable if set
5289

53-
## Methods
90+
### TappdClient Initialization (Legacy API)
5491

55-
### `info(): InfoResponse`
92+
```rust
93+
let client = TappdClient::new(Some("/var/run/tappd.sock"));
94+
```
95+
- `endpoint`: Optional HTTP URL or Unix socket path (`/var/run/tappd.sock` by default)
96+
- Will use the `TAPPD_SIMULATOR_ENDPOINT` environment variable if set
97+
- Supports the legacy tappd.sock API for backwards compatibility
5698

57-
Fetches metadata and measurements about the CVM instance.
99+
## API Methods
58100

59-
### `get_key(path: Option<String>, purpose: Option<String>) -> GetKeyResponse`
101+
### DstackClient Methods
60102

61-
Derives a key for a specified path and optional purpose.
103+
#### `info(): InfoResponse`
104+
Fetches metadata and measurements about the CVM instance.
62105

106+
#### `get_key(path: Option<String>, purpose: Option<String>) -> GetKeyResponse`
107+
Derives a key for a specified path and optional purpose.
63108
- `key`: Private key in hex format
64-
65109
- `signature_chain`: Vec of X.509 certificate chain entries
66110

67-
### `get_quote(report_data: Vec<u8>) -> GetQuoteResponse`
68-
111+
#### `get_quote(report_data: Vec<u8>) -> GetQuoteResponse`
69112
Generates a TDX quote with a custom 64-byte payload.
70-
71113
- `quote`: Hex-encoded quote
72-
73114
- `event_log`: Serialized list of events
74-
75115
- `replay_rtmrs()`: Reconstructs RTMR values from the event log
76116

77-
### `emit_event(event: String, payload: Vec<u8>)`
117+
#### `emit_event(event: String, payload: Vec<u8>)`
78118
Sends an event log with associated binary payload to the runtime.
79119

80-
### `get_tls_key(...) -> GetTlsKeyResponse`
120+
#### `get_tls_key(...) -> GetTlsKeyResponse`
81121
Requests a key and X.509 certificate chain for RA-TLS or server/client authentication.
82122

123+
### TappdClient Methods (Legacy API)
124+
125+
#### `info(): TappdInfoResponse`
126+
Fetches metadata and measurements about the CVM instance.
127+
128+
#### `derive_key(path: &str) -> DeriveKeyResponse`
129+
Derives a key for a specified path.
130+
- `key`: ECDSA P-256 private key in PEM format
131+
- `certificate_chain`: Vec of X.509 certificate chain entries
132+
- `to_bytes()`: Extracts and returns the raw ECDSA P-256 private key bytes (32 bytes)
133+
134+
#### `derive_key_with_subject(path: &str, subject: &str) -> DeriveKeyResponse`
135+
Derives a key with a custom certificate subject.
136+
137+
#### `derive_key_with_subject_and_alt_names(path: &str, subject: Option<&str>, alt_names: Option<Vec<String>>) -> DeriveKeyResponse`
138+
Derives a key with full certificate customization.
139+
140+
#### `get_quote(report_data: Vec<u8>) -> TdxQuoteResponse`
141+
Generates a TDX quote with exactly 64 bytes of raw report data.
142+
83143
### Structures
84144
- `GetKeyResponse`: Holds derived key and signature chain
85145

@@ -104,7 +164,20 @@ cd dstack/sdk/simulator
104164
Set the endpoint in your environment:
105165

106166
```
107-
export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8000
167+
export DSTACK_SIMULATOR_ENDPOINT=/path/to/dstack-simulator/dstack.sock
168+
```
169+
170+
## Examples
171+
172+
See the `examples/` directory for comprehensive usage examples:
173+
174+
- `examples/dstack_client_usage.rs` - Complete example using the current DstackClient API
175+
- `examples/tappd_client_usage.rs` - Complete example using the legacy TappdClient API
176+
177+
Run examples with:
178+
```bash
179+
cargo run --example dstack_client_usage
180+
cargo run --example tappd_client_usage
108181
```
109182

110183
## License
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use dstack_sdk::dstack_client::{DstackClient, TlsKeyConfig};
2+
3+
#[tokio::main]
4+
async fn main() -> anyhow::Result<()> {
5+
// Create a DstackClient with default endpoint (/var/run/dstack.sock)
6+
let client = DstackClient::new(None);
7+
8+
// Or create with a custom endpoint
9+
// let client = DstackClient::new(Some("/custom/path/dstack.sock"));
10+
11+
// Or create with HTTP endpoint for simulator
12+
// let client = DstackClient::new(Some("http://localhost:8000"));
13+
14+
println!("DstackClient created successfully!");
15+
16+
// Example usage (these will fail without a running dstack service):
17+
18+
// 1. Get system info
19+
let info = client.info().await?;
20+
println!("System info retrieved successfully!");
21+
println!(" App ID: {}", info.app_id);
22+
println!(" Instance ID: {}", info.instance_id);
23+
println!(" App Name: {}", info.app_name);
24+
println!(" Device ID: {}", info.device_id);
25+
println!(" Compose Hash: {}", info.compose_hash);
26+
println!(" TCB Info - MRTD: {}", info.tcb_info.mrtd);
27+
println!(" TCB Info - RTMR0: {}", info.tcb_info.rtmr0);
28+
29+
// 2. Derive a key
30+
let response = client
31+
.get_key(Some("my-app".to_string()), Some("encryption".to_string()))
32+
.await?;
33+
println!("Key derived successfully!");
34+
println!(" Key length: {}", response.key.len());
35+
println!(
36+
" Signature chain length: {}",
37+
response.signature_chain.len()
38+
);
39+
40+
// Decode the key
41+
let key_bytes = response.decode_key()?;
42+
println!(" Decoded key bytes length: {}", key_bytes.len());
43+
44+
// 3. Generate TDX quote
45+
let report_data = b"Hello, dstack world!".to_vec();
46+
let response = client.get_quote(report_data).await?;
47+
println!("TDX quote generated successfully!");
48+
println!(" Quote length: {}", response.quote.len());
49+
println!(" Event log length: {}", response.event_log.len());
50+
51+
// Decode the quote
52+
let quote_bytes = response.decode_quote()?;
53+
println!(" Decoded quote bytes length: {}", quote_bytes.len());
54+
55+
// Replay RTMRs from event log
56+
let rtmrs = response.replay_rtmrs()?;
57+
println!(" Replayed RTMRs: {} entries", rtmrs.len());
58+
for (idx, rtmr) in rtmrs.iter() {
59+
println!(" RTMR{}: {}", idx, rtmr);
60+
}
61+
62+
// 4. Emit an event
63+
let event_payload = b"Application started successfully".to_vec();
64+
client
65+
.emit_event("AppStart".to_string(), event_payload)
66+
.await?;
67+
println!("Event emitted successfully!");
68+
69+
// 5. Get TLS key for server authentication
70+
let tls_config = TlsKeyConfig::builder()
71+
.subject("my-app.example.com")
72+
.alt_names(vec![
73+
"www.my-app.com".to_string(),
74+
"api.my-app.com".to_string(),
75+
])
76+
.usage_server_auth(true)
77+
.usage_client_auth(false)
78+
.usage_ra_tls(true)
79+
.build();
80+
81+
let response = client.get_tls_key(tls_config).await?;
82+
println!("TLS key generated successfully!");
83+
println!(" Key length: {}", response.key.len());
84+
println!(
85+
" Certificate chain length: {}",
86+
response.certificate_chain.len()
87+
);
88+
89+
// 6. Get a simple key without purpose
90+
let response = client.get_key(Some("simple-key".to_string()), None).await?;
91+
println!("Simple key derived successfully!");
92+
println!(" Key: {}", response.key);
93+
94+
// 7. Generate quote with minimal report data
95+
let minimal_data = vec![0x01, 0x02, 0x03, 0x04];
96+
let response = client.get_quote(minimal_data).await?;
97+
println!("Minimal quote generated successfully!");
98+
println!(" Quote length: {}", response.quote.len());
99+
println!(" Event log length: {}", response.event_log.len());
100+
101+
// Parse and display event log
102+
let events = response.decode_event_log()?;
103+
println!(" Event log contains {} events", events.len());
104+
for (i, event) in events.iter().enumerate().take(3) {
105+
// Show first 3 events
106+
println!(
107+
" Event {}: IMR={}, Type={}, Event='{}'",
108+
i, event.imr, event.event_type, event.event
109+
);
110+
}
111+
112+
Ok(())
113+
}

0 commit comments

Comments
 (0)