Skip to content
Draft
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 kms/auth-eth/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export async function build(): Promise<FastifyInstance> {
return {
status: 'ok',
kmsContractAddr: kmsContractAddr,
ethRpcUrl: rpcUrl,
gatewayAppId: batch[0],
chainId: batch[1],
appAuthImplementation: batch[2], // NOTE: for backward compatibility
Expand Down
17 changes: 17 additions & 0 deletions kms/auth-mock/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
#
# SPDX-License-Identifier: Apache-2.0

FROM oven/bun:1-alpine
WORKDIR /app

ARG DSTACK_REV
ARG DSTACK_BRANCH=master

RUN apk add --no-cache git
RUN git clone --branch ${DSTACK_BRANCH} https://github.com/Dstack-TEE/dstack.git && \
cd dstack && \
git checkout ${DSTACK_REV}
WORKDIR /app/dstack/kms/auth-mock
RUN bun install --frozen-lockfile
CMD ["bun", "index.ts"]
5 changes: 3 additions & 2 deletions kms/auth-mock/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 70 additions & 15 deletions kms/auth-mock/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ const BootResponseSchema = z.object({
type BootInfo = z.infer<typeof BootInfoSchema>;
type BootResponse = z.infer<typeof BootResponseSchema>;

// authorization policy - configurable via environment variables
// MOCK_POLICY: "allow-all" (default), "deny-kms", "deny-app", "deny-all",
// "allowlist-device", "allowlist-mr"
// MOCK_ALLOWED_DEVICE_IDS: comma-separated device IDs (for allowlist-device policy)
// MOCK_ALLOWED_MR_AGGREGATED: comma-separated MR aggregated values (for allowlist-mr policy)

type MockPolicy = 'allow-all' | 'deny-kms' | 'deny-app' | 'deny-all' | 'allowlist-device' | 'allowlist-mr';

function getPolicy(): MockPolicy {
const policy = process.env.MOCK_POLICY || 'allow-all';
const valid: MockPolicy[] = ['allow-all', 'deny-kms', 'deny-app', 'deny-all', 'allowlist-device', 'allowlist-mr'];
if (!valid.includes(policy as MockPolicy)) {
console.warn(`unknown MOCK_POLICY "${policy}", falling back to allow-all`);
return 'allow-all';
}
return policy as MockPolicy;
}

function parseList(envVar: string): Set<string> {
const raw = process.env[envVar] || '';
return new Set(raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
}

// mock backend class - no blockchain interaction
class MockBackend {
private mockGatewayAppId: string;
Expand All @@ -44,14 +67,45 @@ class MockBackend {
}

async checkBoot(bootInfo: BootInfo, isKms: boolean): Promise<BootResponse> {
// always return success for mock backend
const reason = isKms ? 'mock KMS always allowed' : 'mock app always allowed';

return {
const policy = getPolicy();
const deny = (reason: string): BootResponse => ({
isAllowed: false,
reason,
gatewayAppId: '',
});
const allow = (reason: string): BootResponse => ({
isAllowed: true,
reason,
gatewayAppId: this.mockGatewayAppId,
};
});

switch (policy) {
case 'deny-all':
return deny(`mock policy: deny-all`);
case 'deny-kms':
if (isKms) return deny(`mock policy: deny-kms`);
return allow('mock app allowed (deny-kms policy)');
case 'deny-app':
if (!isKms) return deny(`mock policy: deny-app`);
return allow('mock KMS allowed (deny-app policy)');
case 'allowlist-device': {
const allowed = parseList('MOCK_ALLOWED_DEVICE_IDS');
const deviceId = bootInfo.deviceId.toLowerCase().replace(/^0x/, '');
if (allowed.size === 0) return deny('mock policy: allowlist-device with empty list');
if (!allowed.has(deviceId)) return deny(`mock policy: device ${bootInfo.deviceId} not in allowlist`);
return allow(`mock policy: device ${bootInfo.deviceId} allowed`);
}
case 'allowlist-mr': {
const allowed = parseList('MOCK_ALLOWED_MR_AGGREGATED');
const mr = bootInfo.mrAggregated.toLowerCase().replace(/^0x/, '');
if (allowed.size === 0) return deny('mock policy: allowlist-mr with empty list');
if (!allowed.has(mr)) return deny(`mock policy: mrAggregated ${bootInfo.mrAggregated} not in allowlist`);
return allow(`mock policy: mrAggregated ${bootInfo.mrAggregated} allowed`);
}
case 'allow-all':
default:
return allow(isKms ? 'mock KMS always allowed' : 'mock app always allowed');
}
}

async getGatewayAppId(): Promise<string> {
Expand Down Expand Up @@ -81,10 +135,11 @@ app.get('/', async (c) => {
mockBackend.getChainId(),
mockBackend.getAppImplementation(),
]);

return c.json({
status: 'ok',
kmsContractAddr: process.env.KMS_CONTRACT_ADDR || '0xmockcontract1234567890123456789012345678',
ethRpcUrl: process.env.ETH_RPC_URL || '',
gatewayAppId: batch[0],
chainId: batch[1],
appAuthImplementation: batch[2], // NOTE: for backward compatibility
Expand All @@ -93,15 +148,15 @@ app.get('/', async (c) => {
});
} catch (error) {
console.error('error in health check:', error);
return c.json({
status: 'error',
message: error instanceof Error ? error.message : String(error)
return c.json({
status: 'error',
message: error instanceof Error ? error.message : String(error)
}, 500);
}
});

// app boot authentication
app.post('/bootAuth/app',
app.post('/bootAuth/app',
zValidator('json', BootInfoSchema),
async (c) => {
try {
Expand All @@ -111,7 +166,7 @@ app.post('/bootAuth/app',
instanceId: bootInfo.instanceId,
note: 'always returning success'
});

const result = await mockBackend.checkBoot(bootInfo, false);
return c.json(result);
} catch (error) {
Expand All @@ -136,7 +191,7 @@ app.post('/bootAuth/kms',
instanceId: bootInfo.instanceId,
note: 'always returning success'
});

const result = await mockBackend.checkBoot(bootInfo, true);
return c.json(result);
} catch (error) {
Expand All @@ -155,10 +210,10 @@ app.post('/bootAuth/kms',

// start server
const port = parseInt(process.env.PORT || '3000');
console.log(`starting mock auth server on port ${port}`);
console.log('note: this is a mock backend - all authentications will succeed');
const policy = getPolicy();
console.log(`starting mock auth server on port ${port} (policy: ${policy})`);

export default {
port,
fetch: app.fetch,
};
};
4 changes: 2 additions & 2 deletions kms/dstack-app/builder/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS kms-builder
COPY --from=build-shared pin-packages.sh /build/
COPY --from=build-shared /pin-packages.sh /build/
COPY ./shared/*-pinned-packages.txt /build/
ARG DSTACK_REV
ARG DSTACK_SRC_URL=https://github.com/Dstack-TEE/dstack.git
Expand All @@ -27,7 +27,7 @@ RUN cd dstack && cargo build --release -p dstack-kms --target x86_64-unknown-lin
RUN echo "${DSTACK_REV}" > /build/.GIT_REV

FROM debian:bookworm@sha256:0d8498a0e9e6a60011df39aab78534cfe940785e7c59d19dfae1eb53ea59babe
COPY --from=build-shared pin-packages.sh config-qemu.sh /build/
COPY --from=build-shared /pin-packages.sh /config-qemu.sh /build/
COPY ./shared/qemu-pinned-packages.txt /build/
WORKDIR /build
ARG QEMU_REV=dbcec07c0854bf873d346a09e87e4c993ccf2633
Expand Down
1 change: 1 addition & 0 deletions kms/kms.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mandatory = false
cert_dir = "/etc/kms/certs"
subject_postfix = ".dstack"
admin_token_hash = ""
site_name = ""
# Whether trusted RPCs require the KMS to first attest itself to its own
# auth API. Defaults to true (strict). Set to false ONLY when running KMS
# outside a TEE (e.g. local dev/testing) where the local guest agent socket
Expand Down
12 changes: 11 additions & 1 deletion kms/rpc/proto/kms_rpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ message AppKeyResponse {
string tproxy_app_id = 6;
// Reverse proxy app ID from DstackKms contract.
string gateway_app_id = 7;
// OS Image hash
// OS Image hash
bytes os_image_hash = 8;
}

Expand Down Expand Up @@ -131,6 +131,8 @@ message OnboardRequest {
}

message OnboardResponse {
// k256 public key (secp256k1) inherited from source KMS
bytes k256_pubkey = 1;
}

// Attestation info needed for on-chain KMS authorization.
Expand All @@ -143,6 +145,14 @@ message AttestationInfoResponse {
bytes os_image_hash = 3;
// Attestation mode (e.g. "dstack-tdx", "dstack-gcp-tdx")
string attestation_mode = 4;
// Custom site name for display
string site_name = 5;
// Ethereum RPC URL from auth API
string eth_rpc_url = 6;
// KMS contract address from auth API
string kms_contract_address = 7;
// Raw platform provisioning ID
bytes ppid = 8;
}

// The Onboard RPC service.
Expand Down
2 changes: 2 additions & 0 deletions kms/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ pub(crate) struct KmsConfig {
pub image: ImageConfig,
#[serde(with = "serde_human_bytes")]
pub admin_token_hash: Vec<u8>,
#[serde(default)]
pub site_name: String,
/// Whether trusted RPCs require the KMS to first attest itself to its
/// own auth API. Defaults to `true` (strict). Set `false` only for local
/// dev/testing where the KMS runs outside a TEE and cannot reach a guest
Expand Down
10 changes: 10 additions & 0 deletions kms/src/main_service/upgrade_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ pub(crate) struct BootResponse {
pub(crate) struct AuthApiInfoResponse {
pub status: String,
pub kms_contract_addr: String,
#[serde(default)]
pub eth_rpc_url: String,
pub gateway_app_id: String,
pub chain_id: u64,
pub app_implementation: String,
Expand All @@ -110,6 +112,7 @@ pub(crate) struct GetInfoResponse {
pub is_dev: bool,
pub gateway_app_id: Option<String>,
pub kms_contract_address: Option<String>,
pub eth_rpc_url: Option<String>,
pub chain_id: Option<u64>,
pub app_implementation: Option<String>,
}
Expand Down Expand Up @@ -161,15 +164,22 @@ impl AuthApi {
AuthApi::Dev { dev } => Ok(GetInfoResponse {
is_dev: true,
kms_contract_address: None,
eth_rpc_url: None,
gateway_app_id: Some(dev.gateway_app_id.clone()),
chain_id: None,
app_implementation: None,
}),
AuthApi::Webhook { webhook } => {
let info: AuthApiInfoResponse = http_get(&webhook.url).await?;
let eth_rpc_url = if info.eth_rpc_url.is_empty() {
None
} else {
Some(info.eth_rpc_url.clone())
};
Ok(GetInfoResponse {
is_dev: false,
kms_contract_address: Some(info.kms_contract_addr.clone()),
eth_rpc_url,
chain_id: Some(info.chain_id),
gateway_app_id: Some(info.gateway_app_id.clone()),
app_implementation: Some(info.app_implementation.clone()),
Expand Down
24 changes: 23 additions & 1 deletion kms/src/onboard_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ impl OnboardRpc for OnboardHandler {
)
.await
.context("Failed to onboard")?;
let k256_pubkey = keys.k256_key.verifying_key().to_sec1_bytes().to_vec();
keys.store(&self.state.config)
.context("Failed to store keys")?;
Ok(OnboardResponse {})
Ok(OnboardResponse { k256_pubkey })
}

async fn get_attestation_info(self) -> Result<AttestationInfoResponse> {
Expand Down Expand Up @@ -135,12 +136,33 @@ impl OnboardRpc for OnboardHandler {
let app_info = verified
.decode_app_info_ex(false, &info.vm_config)
.context("Failed to decode app info")?;
let ppid = verified
.report
.tdx_report()
.map(|report| report.ppid.to_vec())
.unwrap_or_default();

let (eth_rpc_url, kms_contract_address) = match self.state.config.auth_api.get_info().await
{
Ok(info) => (
info.eth_rpc_url.unwrap_or_default(),
info.kms_contract_address.unwrap_or_default(),
),
Err(err) => {
tracing::warn!("failed to get auth api info: {err}");
(String::new(), String::new())
}
};

Ok(AttestationInfoResponse {
device_id: app_info.device_id,
mr_aggregated: app_info.mr_aggregated.to_vec(),
os_image_hash: app_info.os_image_hash,
attestation_mode,
site_name: self.state.config.site_name.clone(),
eth_rpc_url,
kms_contract_address,
ppid,
})
}

Expand Down
Loading
Loading