diff --git a/kms/auth-eth/src/server.ts b/kms/auth-eth/src/server.ts index 16f8cbc87..d2db629b9 100644 --- a/kms/auth-eth/src/server.ts +++ b/kms/auth-eth/src/server.ts @@ -59,6 +59,7 @@ export async function build(): Promise { return { status: 'ok', kmsContractAddr: kmsContractAddr, + ethRpcUrl: rpcUrl, gatewayAppId: batch[0], chainId: batch[1], appAuthImplementation: batch[2], // NOTE: for backward compatibility diff --git a/kms/auth-mock/Dockerfile b/kms/auth-mock/Dockerfile new file mode 100644 index 000000000..095ff1b29 --- /dev/null +++ b/kms/auth-mock/Dockerfile @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2025 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"] diff --git a/kms/auth-mock/bun.lock b/kms/auth-mock/bun.lock index 7d66b822a..b525e9fbd 100644 --- a/kms/auth-mock/bun.lock +++ b/kms/auth-mock/bun.lock @@ -1,11 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "auth-mock", "dependencies": { "@hono/zod-validator": "0.2.2", - "hono": "4.8.5", + "hono": "4.12.7", "zod": "3.25.76", }, "devDependencies": { @@ -212,7 +213,7 @@ "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "hono": ["hono@4.8.5", "", {}, "sha512-Up2cQbtNz1s111qpnnECdTGqSIUIhZJMLikdKkshebQSEBcoUKq6XJayLGqSZWidiH0zfHRCJqFu062Mz5UuRA=="], + "hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="], "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], diff --git a/kms/auth-mock/index.ts b/kms/auth-mock/index.ts index cef8becb5..3636650de 100644 --- a/kms/auth-mock/index.ts +++ b/kms/auth-mock/index.ts @@ -30,6 +30,29 @@ const BootResponseSchema = z.object({ type BootInfo = z.infer; type BootResponse = z.infer; +// 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 { + 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; @@ -44,14 +67,45 @@ class MockBackend { } async checkBoot(bootInfo: BootInfo, isKms: boolean): Promise { - // 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 { @@ -85,6 +139,7 @@ app.get('/', async (c) => { 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 @@ -155,8 +210,8 @@ 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, diff --git a/kms/kms.toml b/kms/kms.toml index 70b2b7177..2d84b469c 100644 --- a/kms/kms.toml +++ b/kms/kms.toml @@ -26,6 +26,7 @@ mandatory = false cert_dir = "/etc/kms/certs" subject_postfix = ".dstack" admin_token_hash = "" +site_name = "" [core.image] verify = true diff --git a/kms/rpc/proto/kms_rpc.proto b/kms/rpc/proto/kms_rpc.proto index adaaa5f52..eb56f7307 100644 --- a/kms/rpc/proto/kms_rpc.proto +++ b/kms/rpc/proto/kms_rpc.proto @@ -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. @@ -143,6 +145,12 @@ 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; } // The Onboard RPC service. diff --git a/kms/src/config.rs b/kms/src/config.rs index 3eaa2d117..ce9bdf6cc 100644 --- a/kms/src/config.rs +++ b/kms/src/config.rs @@ -40,6 +40,8 @@ pub(crate) struct KmsConfig { pub image: ImageConfig, #[serde(with = "serde_human_bytes")] pub admin_token_hash: Vec, + #[serde(default)] + pub site_name: String, } impl KmsConfig { diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index d2b64016b..e41e17868 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -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, @@ -110,6 +112,7 @@ pub(crate) struct GetInfoResponse { pub is_dev: bool, pub gateway_app_id: Option, pub kms_contract_address: Option, + pub eth_rpc_url: Option, pub chain_id: Option, pub app_implementation: Option, } @@ -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()), diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index 93eeb5629..79709a218 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -98,9 +98,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 { @@ -136,11 +137,26 @@ impl OnboardRpc for OnboardHandler { .decode_app_info_ex(false, &info.vm_config) .context("Failed to decode app info")?; + 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, }) } diff --git a/kms/src/www/onboard.html b/kms/src/www/onboard.html index bca53d2ac..6f7b6e010 100644 --- a/kms/src/www/onboard.html +++ b/kms/src/www/onboard.html @@ -145,6 +145,19 @@ color: #333; } + .chain-info { + background-color: #f8f4e8; + border: 1px solid #ddc; + border-radius: 4px; + padding: 15px; + margin-bottom: 20px; + } + + .chain-info h3 { + margin-top: 0; + color: #444; + } + .loading { color: #888; font-style: italic; @@ -154,7 +167,7 @@
-

dstack KMS Setup

+

{{ siteName || 'dstack KMS Setup' }}

Loading attestation info...
Attestation info: {{ attestationError }}
@@ -178,6 +191,18 @@

Attestation Info (for on-chain registration)

+
+

Chain Info

+
+ ETH RPC URL: + {{ attestationInfo.eth_rpc_url }} +
+
+ KMS Contract: + {{ attestationInfo.kms_contract_address }} +
+
+
@@ -261,7 +286,8 @@

Onboard from an Existing KMS Instance

setupFinished: false, attestationInfo: null, attestationLoading: true, - attestationError: '' + attestationError: '', + siteName: '' } }, async mounted() { @@ -271,6 +297,10 @@

Onboard from an Existing KMS Instance

this.attestationError = data.error; } else { this.attestationInfo = data; + if (data.site_name) { + this.siteName = data.site_name; + document.title = data.site_name; + } } } catch (err) { this.attestationError = err.message; @@ -310,6 +340,9 @@

Onboard from an Existing KMS Instance

if (data.error) throw new Error(data.error); this.success = 'Onboarding successful!'; + this.result = JSON.stringify({ + k256Pubkey: '0x' + data.k256_pubkey, + }, null, 2); this.error = ''; } catch (err) { this.error = err.message;