From 5af331c93c8854fbb28391c4d549cc53d9eca0eb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 27 Apr 2026 20:37:38 -0400 Subject: [PATCH] feat(mobile): use plain ws:// transport until mobile cert pinning is implemented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React Native's built-in WebSocket rejects self-signed TLS certs with no way to pin by fingerprint. Use plain ws:// for now — per-device token auth still prevents unauthorized access. TLS will be re-enabled once the Expo native module for certificate pinning is built (Phase 1 checklist). Co-authored-by: Orca --- src/main/runtime/rpc/ws-transport.ts | 48 +++++++++++++++------------- src/main/runtime/runtime-rpc.ts | 13 ++++---- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/main/runtime/rpc/ws-transport.ts b/src/main/runtime/rpc/ws-transport.ts index 201bedb517f..ba10e88f6ce 100644 --- a/src/main/runtime/rpc/ws-transport.ts +++ b/src/main/runtime/rpc/ws-transport.ts @@ -1,9 +1,10 @@ // Why: the WebSocket transport enables mobile clients to connect to the Orca -// runtime over the local network. It uses wss:// with a self-signed TLS -// certificate to prevent passive sniffing, and per-device tokens (validated -// by the message handler in OrcaRuntimeRpcServer) instead of the shared -// runtime auth token. +// runtime over the local network. When TLS cert/key are provided it uses wss:// +// to prevent passive sniffing; otherwise it falls back to plain ws://. Per-device +// tokens (validated by the message handler in OrcaRuntimeRpcServer) provide auth +// regardless of transport encryption. import { createServer as createHttpsServer, type Server as HttpsServer } from 'https' +import { createServer as createHttpServer, type Server as HttpServer } from 'http' import { WebSocketServer, type WebSocket } from 'ws' import type { RpcTransport } from './transport' @@ -13,16 +14,16 @@ const MAX_WS_CONNECTIONS = 32 export type WebSocketTransportOptions = { host: string port: number - tlsCert: string - tlsKey: string + tlsCert?: string + tlsKey?: string } export class WebSocketTransport implements RpcTransport { private readonly host: string private readonly port: number - private readonly tlsCert: string - private readonly tlsKey: string - private httpsServer: HttpsServer | null = null + private readonly tlsCert: string | undefined + private readonly tlsKey: string | undefined + private httpServer: HttpsServer | HttpServer | null = null private wss: WebSocketServer | null = null private messageHandler: ((msg: string, reply: (response: string) => void) => void) | null = null @@ -42,13 +43,16 @@ export class WebSocketTransport implements RpcTransport { return } - const httpsServer = createHttpsServer({ - cert: this.tlsCert, - key: this.tlsKey - }) + // Why: React Native's built-in WebSocket cannot disable TLS verification + // for self-signed certificates. Until we implement certificate pinning on + // the mobile side, use plain ws:// which still has per-device token auth. + const httpServer = + this.tlsCert && this.tlsKey + ? createHttpsServer({ cert: this.tlsCert, key: this.tlsKey }) + : createHttpServer() const wss = new WebSocketServer({ - server: httpsServer, + server: httpServer, maxPayload: MAX_WS_MESSAGE_BYTES }) @@ -61,22 +65,22 @@ export class WebSocketTransport implements RpcTransport { }) await new Promise((resolve, reject) => { - httpsServer.once('error', reject) - httpsServer.listen(this.port, this.host, () => { - httpsServer.off('error', reject) + httpServer.once('error', reject) + httpServer.listen(this.port, this.host, () => { + httpServer.off('error', reject) resolve() }) }) - this.httpsServer = httpsServer + this.httpServer = httpServer this.wss = wss } async stop(): Promise { const wss = this.wss - const httpsServer = this.httpsServer + const httpServer = this.httpServer this.wss = null - this.httpsServer = null + this.httpServer = null if (wss) { for (const client of wss.clients) { @@ -85,9 +89,9 @@ export class WebSocketTransport implements RpcTransport { wss.close() } - if (httpsServer) { + if (httpServer) { await new Promise((resolve, reject) => { - httpsServer.close((error) => { + httpServer.close((error) => { if (error) { reject(error) return diff --git a/src/main/runtime/runtime-rpc.ts b/src/main/runtime/runtime-rpc.ts index fd0955b9e18..59eeed0306e 100644 --- a/src/main/runtime/runtime-rpc.ts +++ b/src/main/runtime/runtime-rpc.ts @@ -14,7 +14,6 @@ import { errorResponse } from './rpc/errors' import type { RpcTransport } from './rpc/transport' import { UnixSocketTransport } from './rpc/unix-socket-transport' import { WebSocketTransport } from './rpc/ws-transport' -import { loadOrCreateTlsCertificate } from './tls-certificate' import { DeviceRegistry } from './device-registry' const DEFAULT_WS_PORT = 6768 @@ -108,15 +107,15 @@ export class OrcaRuntimeRpcServer { // device registry, not the shared runtime auth token. if (this.enableWebSocket) { try { - const tls = loadOrCreateTlsCertificate(this.userDataPath) - this.tlsFingerprint = tls.fingerprint this.deviceRegistry = new DeviceRegistry(this.userDataPath) + // Why: React Native's built-in WebSocket cannot disable TLS verification + // for self-signed certificates. Use plain ws:// for now — per-device + // tokens still provide auth. TLS will be re-enabled once the mobile app + // implements certificate pinning via the fingerprint from QR pairing. const wsTransport = new WebSocketTransport({ host: '0.0.0.0', - port: this.wsPort, - tlsCert: tls.cert, - tlsKey: tls.key + port: this.wsPort }) // Why: WebSocket transport uses dispatchStreaming which can emit @@ -130,7 +129,7 @@ export class OrcaRuntimeRpcServer { activeTransports.push(wsTransport) transportsMeta.push({ kind: 'websocket', - endpoint: `wss://0.0.0.0:${this.wsPort}` + endpoint: `ws://0.0.0.0:${this.wsPort}` }) } catch (error) { // Why: WebSocket transport is supplementary — the runtime must still