diff --git a/src/gateway/server/ws-connection/connect-policy.test.ts b/src/gateway/server/ws-connection/connect-policy.test.ts index 51e1b649126..d28079f5672 100644 --- a/src/gateway/server/ws-connection/connect-policy.test.ts +++ b/src/gateway/server/ws-connection/connect-policy.test.ts @@ -5,6 +5,7 @@ import { evaluateMissingDeviceIdentity, isTrustedProxyControlUiOperatorAuth, resolveControlUiAuthPolicy, + resolveInternalBackendClientAttestation, shouldSkipBackendSelfPairing, shouldSkipControlUiPairing, } from "./connect-policy.js"; @@ -643,4 +644,56 @@ describe("ws connect policy", () => { }), ).toBe(false); }); + + test("promotes bootstrap-paired backend clients to internal attestation", () => { + const backendConnect: ConnectParams = { + client: { + id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, + mode: GATEWAY_CLIENT_MODES.BACKEND, + version: "1.0.0", + platform: "node", + }, + minProtocol: 1, + maxProtocol: 1, + }; + + expect( + resolveInternalBackendClientAttestation({ + connectParams: backendConnect, + hasBrowserOriginHeader: false, + initialIsInternalBackendClient: false, + authMethod: "bootstrap-token", + deviceTokenIssued: true, + }), + ).toBe(true); + + expect( + resolveInternalBackendClientAttestation({ + connectParams: backendConnect, + hasBrowserOriginHeader: true, + initialIsInternalBackendClient: false, + authMethod: "bootstrap-token", + deviceTokenIssued: true, + }), + ).toBe(false); + + expect( + resolveInternalBackendClientAttestation({ + connectParams: { + client: { + id: "desktop", + mode: GATEWAY_CLIENT_MODES.TEST, + version: "1.0.0", + platform: "node", + }, + minProtocol: 1, + maxProtocol: 1, + }, + hasBrowserOriginHeader: false, + initialIsInternalBackendClient: false, + authMethod: "bootstrap-token", + deviceTokenIssued: true, + }), + ).toBe(false); + }); }); diff --git a/src/gateway/server/ws-connection/connect-policy.ts b/src/gateway/server/ws-connection/connect-policy.ts index b12f2f051d0..6d038d8f2ba 100644 --- a/src/gateway/server/ws-connection/connect-policy.ts +++ b/src/gateway/server/ws-connection/connect-policy.ts @@ -113,6 +113,25 @@ export function shouldSkipBackendSelfPairing(params: { ); } +export function resolveInternalBackendClientAttestation(params: { + connectParams: ConnectParams; + hasBrowserOriginHeader: boolean; + initialIsInternalBackendClient: boolean; + authMethod: GatewayAuthResult["method"]; + deviceTokenIssued: boolean; +}): boolean { + if (params.initialIsInternalBackendClient) { + return true; + } + const isGatewayBackendClient = + params.connectParams.client.id === GATEWAY_CLIENT_IDS.GATEWAY_CLIENT && + params.connectParams.client.mode === GATEWAY_CLIENT_MODES.BACKEND; + if (!isGatewayBackendClient || params.hasBrowserOriginHeader) { + return false; + } + return params.authMethod === "bootstrap-token" && params.deviceTokenIssued; +} + export type MissingDeviceIdentityDecision = | { kind: "allow" } | { kind: "reject-control-ui-insecure-auth" } diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 8012ce582d7..6d726f77583 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -83,6 +83,7 @@ import { evaluateMissingDeviceIdentity, isTrustedProxyControlUiOperatorAuth, resolveControlUiAuthPolicy, + resolveInternalBackendClientAttestation, shouldSkipBackendSelfPairing, shouldSkipControlUiPairing, } from "./connect-policy.js"; @@ -891,6 +892,13 @@ export function attachGatewayWsMessageHandler(params: { const deviceToken = device ? await ensureDeviceToken({ deviceId: device.id, role, scopes }) : null; + const attestedInternalBackendClient = resolveInternalBackendClientAttestation({ + connectParams, + hasBrowserOriginHeader, + initialIsInternalBackendClient: isInternalBackendClient, + authMethod, + deviceTokenIssued: deviceToken !== null, + }); if (role === "node") { const cfg = loadConfig(); @@ -995,7 +1003,7 @@ export function attachGatewayWsMessageHandler(params: { canvasHostUrl, canvasCapability, canvasCapabilityExpiresAtMs, - isInternalBackendClient, + isInternalBackendClient: attestedInternalBackendClient, }; setSocketMaxPayload(socket, MAX_PAYLOAD_BYTES); setClient(nextClient);