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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- sdk: `verify_signature` and `verify_signature_chain` in all four SDKs, replacing the removed guest-agent `Verify` RPC. `verify_signature_chain` is new capability rather than a port: it walks all three links of a `Sign` signature chain -- payload signature, the app root key attesting `"{purpose}:{hex(pubkey)}"`, and the KMS root attesting that app root for this `app_id` -- and requires the chain to anchor at a KMS root public key **the caller supplies**. That anchor has to come from somewhere independently trusted (the `DstackKms` contract's `kmsInfo().k256Pubkey`, or a pinned value); read it from the KMS being checked and an attacker who can answer that query can also mint a self-consistent chain. The four ports are pinned against one committed set of test vectors, `sdk/tests/vectors/signature_chain.json`, generated from the real KMS and guest-agent primitives -- this repo has shipped cross-language crypto drift twice already
- sdk: `AppCompose` in the Go SDK gained `init_script`, `storage_fs`, `swap_size`, `event_log_version`, `port_policy` and `verity_volumes`, and `Requirements` gained `gpu_policy` in the Go and Python SDKs
- shared API authentication (`dstack-api-auth`) protecting the full VMM HTTP/pRPC/UI surface and unifying Gateway/KMS admin auth: bearer/`X-Admin-Token`/HTTP Basic/bcrypt htpasswd, constant-time verification (#796)
- gateway: `Admin.Status` reports `health_gating`, so an operator can see whether this node's health polling is switched on. With it off, instances that opted in sit at `unknown` forever and are all in rotation, which is otherwise indistinguishable on the dashboard from being held out pending a first answer
Expand Down Expand Up @@ -39,6 +40,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- guest-agent: `Worker.GetAttestationForAppKey` is replaced by `Worker.AttestAppKey`. The old method returned a `GetQuoteResponse`, so restricting `GetQuote` to Intel TDX left it unable to answer anywhere else — and the external listener with no way to attest an app key at all, since `Attest` is on the internal socket and an external caller could not use it anyway, not knowing the app key's public key until the agent derives it. `AttestAppKey` takes the same request and returns an `AttestResponse`, on every platform. This is a breaking change to an RPC present since v0.5.7; it ships no SDK method and has no known callers


### Removed
- guest-agent: the `Verify` RPC (`/Verify`), present since v0.5.6. Checking a signature needs no key material and no attestation, and the agent's verdict arrived over the socket unattested -- a caller who believed the TEE was vouching for it was mistaken, and one who did not gained nothing over checking the signature locally. It was also inbound attack surface, parsing attacker-supplied keys and signatures inside the TEE for no benefit. `Sign` stays server-side, because it needs a key only the TEE holds. **Breaking:** an SDK pinned at 0.5.x that calls `/Verify` against a 0.6+ guest agent gets an unknown-method error; update to an SDK that verifies locally. The client-side `verify()` method is gone from the Rust, Python, Go and JavaScript SDKs, replaced by the standalone `verify_signature` above -- it never needed a client connection in the first place. Verification also became stricter in one respect: non-canonical high-S secp256k1 signatures are now rejected explicitly everywhere. `k256` accepted only the canonical form, so the Rust agent already behaved this way, but a naive port to Python or Go would have silently accepted both `(r, s)` and `(r, n-s)` for the same message


## [0.5.5] - 2025-10-20

### Added
Expand Down
1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ path = [
"tools/sca/examples/hello-c/config.json",
"tools/sca/examples/heartbeat/rootfs/etc/heartbeat/interval",
"sdk/simulator/*.json",
"sdk/tests/vectors/*.json",
"sdk/go/go.sum",
"sdk/go/ratls/go.sum",
"dstack/kms/dstack-app/builder/shared/builder-pinned-packages.txt",
Expand Down
18 changes: 5 additions & 13 deletions dstack/guest-agent/rpc/proto/agent_rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ service DstackGuest {
// Sign a payload
rpc Sign(SignRequest) returns (SignResponse) {}

// Verify a signature
rpc Verify(VerifyRequest) returns (VerifyResponse) {}
// Removed in v0.6.0: `rpc Verify(VerifyRequest) returns (VerifyResponse)`.
// Signature verification needs no key material and no attestation, and the
// agent's answer arrives over the socket unattested, so a caller gained
// nothing over checking the signature itself. The SDKs now do it locally --
// see `verify_signature` / `verify_signature_chain`. Do not reuse the name.

// Get the guest agent version
rpc Version(google.protobuf.Empty) returns (WorkerVersion) {}
Expand Down Expand Up @@ -328,17 +331,6 @@ message SignResponse {
bytes public_key = 3;
}

message VerifyRequest {
string algorithm = 1;
bytes data = 2;
bytes signature = 3;
bytes public_key = 4;
}

message VerifyResponse {
bool valid = 1;
}

message AttestAppKeyRequest {
string algorithm = 1;
}
96 changes: 3 additions & 93 deletions dstack/guest-agent/src/rpc_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ use dstack_guest_agent_rpc::{
AppInfo, AttestAppKeyRequest, AttestResponse, DeriveK256KeyResponse, DeriveKeyArgs, GetKeyArgs,
GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, GetTlsKeyResponse, GpuInfoResponse,
HealthResponse, RawQuoteArgs, SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse,
VerifyRequest, VerifyResponse, WorkerVersion,
WorkerVersion,
};
use dstack_types::{AppKeys, SysConfig, GPU_ATTESTATION_OUTPUT};
use ed25519_dalek::ed25519::signature::hazmat::{PrehashSigner, PrehashVerifier};
use ed25519_dalek::{
Signer as Ed25519Signer, SigningKey as Ed25519SigningKey, Verifier as Ed25519Verifier,
};
use ed25519_dalek::ed25519::signature::hazmat::PrehashSigner;
use ed25519_dalek::{Signer as Ed25519Signer, SigningKey as Ed25519SigningKey};
use fs_err as fs;
use k256::ecdsa::SigningKey;
use or_panic::ResultOrPanic;
Expand Down Expand Up @@ -445,40 +443,6 @@ impl DstackGuestRpc for InternalRpcHandler {
})
}

async fn verify(self, request: VerifyRequest) -> Result<VerifyResponse> {
let algorithm = normalize_algorithm(&request.algorithm);
let valid = match algorithm {
"ed25519" => {
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(
&request
.public_key
.as_slice()
.try_into()
.ok()
.context("invalid public key")?,
)?;
let signature = ed25519_dalek::Signature::from_slice(&request.signature)?;
verifying_key.verify(&request.data, &signature).is_ok()
}
"secp256k1" => {
let verifying_key =
k256::ecdsa::VerifyingKey::from_sec1_bytes(&request.public_key)?;
let signature = k256::ecdsa::Signature::from_slice(&request.signature)?;
verifying_key.verify(&request.data, &signature).is_ok()
}
"secp256k1_prehashed" => {
let verifying_key =
k256::ecdsa::VerifyingKey::from_sec1_bytes(&request.public_key)?;
let signature = k256::ecdsa::Signature::from_slice(&request.signature)?;
verifying_key
.verify_prehash(&request.data, &signature)
.is_ok()
}
_ => return Err(anyhow::anyhow!("Unsupported algorithm")),
};
Ok(VerifyResponse { valid })
}

async fn attest(self, request: RawQuoteArgs) -> Result<AttestResponse> {
let report_data = pad64(&request.report_data).context("Report data is too long")?;
self.state.attest_response(report_data)
Expand Down Expand Up @@ -1028,60 +992,6 @@ pNs85uhOZE8z2jr8Pg==
)
}

#[tokio::test]
async fn test_verify_ed25519_success() {
let (state, _guard) = setup_test_state().await;
let handler = InternalRpcHandler {
state: state.clone(),
};
let data_to_sign = b"test message for ed25519";
let sign_request = SignRequest {
algorithm: "ed25519".to_string(),
data: data_to_sign.to_vec(),
};

let sign_response = handler.sign(sign_request).await.unwrap();

let verify_request = VerifyRequest {
algorithm: "ed25519".to_string(),
data: data_to_sign.to_vec(),
signature: sign_response.signature,
public_key: sign_response.public_key,
};
let handler = InternalRpcHandler {
state: state.clone(),
};
let verify_response = handler.verify(verify_request).await.unwrap();
assert!(verify_response.valid);
}

#[tokio::test]
async fn test_verify_secp256k1_success() {
let (state, _guard) = setup_test_state().await;
let handler = InternalRpcHandler {
state: state.clone(),
};
let data_to_sign = b"test message for secp256k1";
let sign_request = SignRequest {
algorithm: "secp256k1".to_string(),
data: data_to_sign.to_vec(),
};

let sign_response = handler.sign(sign_request).await.unwrap();

let verify_request = VerifyRequest {
algorithm: "secp256k1".to_string(),
data: data_to_sign.to_vec(),
signature: sign_response.signature,
public_key: sign_response.public_key,
};
let handler = InternalRpcHandler {
state: state.clone(),
};
let verify_response = handler.verify(verify_request).await.unwrap();
assert!(verify_response.valid);
}

#[tokio::test]
async fn test_sign_ed25519_success() {
let (state, _guard) = setup_test_state().await;
Expand Down
Loading
Loading